From ea9d71f81a062628f37829d2c8c022e1d90ca617 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=98=A5=E5=B8=8C?= Date: Sat, 12 Sep 2020 02:30:18 +0800 Subject: [PATCH] =?UTF-8?q?refactor:=20=F0=9F=92=A1=20support=20scope=20in?= =?UTF-8?q?fo=20&=20use=20middleware=20style=20plugin?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../code-generator/src/parser/SchemaParser.ts | 2 +- .../component/rax/containerInitState.ts | 4 +- .../rax/containerInjectDataSourceEngine.ts | 14 +- .../src/plugins/component/rax/jsx.ts | 146 ++++----- .../component/react/containerDataSource.ts | 5 +- .../component/react/containerInitState.ts | 4 +- .../component/recore/pageDataSource.ts | 7 +- .../plugins/component/recore/pageVmBody.ts | 16 +- .../src/plugins/project/constants.ts | 4 +- packages/code-generator/src/types/core.ts | 71 ++-- packages/code-generator/src/types/index.ts | 1 + packages/code-generator/src/types/jsx.ts | 29 ++ packages/code-generator/src/utils/Scope.ts | 27 ++ .../code-generator/src/utils/aopHelper.ts | 27 ++ .../code-generator/src/utils/compositeType.ts | 103 ++++-- packages/code-generator/src/utils/jsSlot.ts | 28 +- .../code-generator/src/utils/nodeToJSX.ts | 306 ++++++++++-------- packages/code-generator/src/utils/schema.ts | 78 ++++- 18 files changed, 541 insertions(+), 331 deletions(-) create mode 100644 packages/code-generator/src/types/jsx.ts create mode 100644 packages/code-generator/src/utils/Scope.ts create mode 100644 packages/code-generator/src/utils/aopHelper.ts diff --git a/packages/code-generator/src/parser/SchemaParser.ts b/packages/code-generator/src/parser/SchemaParser.ts index 426479383..4f7781926 100644 --- a/packages/code-generator/src/parser/SchemaParser.ts +++ b/packages/code-generator/src/parser/SchemaParser.ts @@ -8,7 +8,7 @@ import { IPageMeta } from '../types'; import { SUPPORT_SCHEMA_VERSION_LIST } from '../const'; -import { handleSubNodes } from '../utils/nodeToJSX'; +import { handleSubNodes } from '../utils/schema'; import { uniqueArray } from '../utils/common'; import { diff --git a/packages/code-generator/src/plugins/component/rax/containerInitState.ts b/packages/code-generator/src/plugins/component/rax/containerInitState.ts index c458d73d1..9974281bb 100644 --- a/packages/code-generator/src/plugins/component/rax/containerInitState.ts +++ b/packages/code-generator/src/plugins/component/rax/containerInitState.ts @@ -1,6 +1,7 @@ import { CLASS_DEFINE_CHUNK_NAME, DEFAULT_LINK_AFTER } from '../../../const/generator'; import { generateCompositeType } from '../../../utils/compositeType'; +import Scope from '../../../utils/Scope'; import { BuilderComponentPlugin, @@ -29,12 +30,13 @@ const pluginFactory: BuilderComponentPluginFactory = (config?) => }; const ir = next.ir as IContainerInfo; + const scope = Scope.createRootScope(); if (ir.state) { const state = ir.state; const fields = Object.keys(state).map((stateName) => { // TODO: 这里用什么 handlers? - const value = generateCompositeType(state[stateName], {}); + const value = generateCompositeType(state[stateName], scope); return `${stateName}: ${value}`; }); diff --git a/packages/code-generator/src/plugins/component/rax/containerInjectDataSourceEngine.ts b/packages/code-generator/src/plugins/component/rax/containerInjectDataSourceEngine.ts index 92285fbfd..3dea73068 100644 --- a/packages/code-generator/src/plugins/component/rax/containerInjectDataSourceEngine.ts +++ b/packages/code-generator/src/plugins/component/rax/containerInjectDataSourceEngine.ts @@ -2,6 +2,7 @@ import { CompositeValue, JSExpression, DataSourceConfig, isJSExpression, isJSFun import changeCase from 'change-case'; import { CLASS_DEFINE_CHUNK_NAME, COMMON_CHUNK_NAME } from '../../../const/generator'; +import Scope from '../../../utils/Scope'; import { BuilderComponentPlugin, @@ -9,6 +10,7 @@ import { ChunkType, FileType, ICodeStruct, + IScope, } from '../../../types'; import { generateCompositeType } from '../../../utils/compositeType'; @@ -31,6 +33,7 @@ const pluginFactory: BuilderComponentPluginFactory = (config?) => ...pre, }; + const scope = Scope.createRootScope(); const dataSourceConfig = isContainerSchema(pre.ir) ? pre.ir.dataSource : null; const dataSourceItems: DataSourceConfig[] = (dataSourceConfig && dataSourceConfig.list) || []; const dataSourceEngineOptions = { runtimeConfig: true }; @@ -80,7 +83,7 @@ const pluginFactory: BuilderComponentPluginFactory = (config?) => _dataSourceEngine = __$$createDataSourceEngine( this._dataSourceConfig, this._context, - ${generateCompositeType(dataSourceEngineOptions)} + ${generateCompositeType(dataSourceEngineOptions, scope)} );`, linkAfter: [CLASS_DEFINE_CHUNK_NAME.Start], }); @@ -108,11 +111,12 @@ _defineDataSourceConfig() { list: [ ...dataSourceItems.map((item) => ({ ...item, - isInit: wrapAsFunction(item.isInit), - options: wrapAsFunction(item.options), + isInit: wrapAsFunction(item.isInit, scope), + options: wrapAsFunction(item.options, scope), })), ], }, + scope, { handlers: { function: (jsFunc) => parseExpressionConvertThis2Context(jsFunc.value, '__$$context'), @@ -132,7 +136,7 @@ _defineDataSourceConfig() { export default pluginFactory; -function wrapAsFunction(value: CompositeValue): CompositeValue { +function wrapAsFunction(value: CompositeValue, scope: IScope): CompositeValue { if (isJSExpression(value) || isJSFunction(value)) { return { type: 'JSExpression', @@ -142,6 +146,6 @@ function wrapAsFunction(value: CompositeValue): CompositeValue { return { type: 'JSExpression', - value: `function(){return((${generateCompositeType(value)}))}`, + value: `function(){return((${generateCompositeType(value, scope)}))}`, }; } diff --git a/packages/code-generator/src/plugins/component/rax/jsx.ts b/packages/code-generator/src/plugins/component/rax/jsx.ts index 3204ebccc..33420aaae 100644 --- a/packages/code-generator/src/plugins/component/rax/jsx.ts +++ b/packages/code-generator/src/plugins/component/rax/jsx.ts @@ -14,16 +14,19 @@ import { IContainerInfo, PIECE_TYPE, HandlerSet, + IScope, + NodeGeneratorConfig, + NodePlugin, + AttrPlugin, } from '../../../types'; import { RAX_CHUNK_NAME } from './const'; import { COMMON_CHUNK_NAME } from '../../../const/generator'; import { generateExpression } from '../../../utils/jsExpression'; -import { createNodeGenerator, generateReactCtrlLine, generateAttr } from '../../../utils/nodeToJSX'; +import { createNodeGenerator, generateConditionReactCtrl, generateReactExprInJS } from '../../../utils/nodeToJSX'; import { generateCompositeType } from '../../../utils/compositeType'; - -import { IScopeBindings, ScopeBindings } from '../../../utils/ScopeBindings'; +import Scope from '../../../utils/Scope'; import { parseExpression, parseExpressionConvertThis2Context, @@ -48,6 +51,7 @@ const pluginFactory: BuilderComponentPluginFactory = (config?) => }; const ir = next.ir as IContainerInfo; + const rootScope = Scope.createRootScope(); // Rax 构建到小程序的时候,不能给组件起起别名,得直接引用,故这里将所有的别名替换掉 // 先收集下所有的 alias 的映射 @@ -70,44 +74,24 @@ const pluginFactory: BuilderComponentPluginFactory = (config?) => // 2. 小程序出码的时候,很容易出现 Uncaught TypeError: Cannot read property 'avatar' of undefined 这样的异常(如下图的 50 行) -- 因为若直接出码,Rax 构建到小程序的时候会立即计算所有在视图中用到的变量 // 3. 通过 this.xxx 能拿到的东西太多了,而且自定义的 methods 可能会无意间破坏 Rax 框架或小程序框架在页面 this 上的东东 const customHandlers: HandlerSet = { - expression(this: CustomHandlerSet, input: JSExpression) { - return transformJsExpr(generateExpression(input), this); + expression(input: JSExpression, scope: IScope) { + return transformJsExpr(generateExpression(input), scope); }, - function(input) { - return transformThis2Context(input.value || 'null', this); + function(input, scope: IScope) { + return transformThis2Context(input.value || 'null', scope); }, - loopDataExpr(input) { - return typeof input === 'string' ? transformLoopExpr(input, this) : ''; - }, - tagName: mapComponentNameToAliasOrKeepIt, - nodeAttr: generateNodeAttrForRax, }; // 创建代码生成器 const commonNodeGenerator = createNodeGenerator({ handlers: customHandlers, - plugins: [generateReactCtrlLine], + tagMapping: mapComponentNameToAliasOrKeepIt, + nodePlugins: [generateReactExprInJS, generateConditionReactCtrl, generateRaxLoopCtrl], + attrPlugins: [generateNodeAttrForRax], }); - const raxCodeGenerator = (node: NodeSchema): string => { - if (node.loop) { - const loopItemName = node.loopArgs?.[0] || 'item'; - const loopIndexName = node.loopArgs?.[1] || 'index'; - - return runInNewScope({ - scopeHost: customHandlers, - newScopeOwnVariables: [loopItemName, loopIndexName], - run: () => commonNodeGenerator(node), - }); - } - - return commonNodeGenerator(node); - }; - - customHandlers.node = raxCodeGenerator; - // 生成 JSX 代码 - const jsxContent = raxCodeGenerator(ir); + const jsxContent = commonNodeGenerator(ir, rootScope); next.chunks.push({ type: ChunkType.STRING, @@ -164,11 +148,7 @@ const pluginFactory: BuilderComponentPluginFactory = (config?) => export default pluginFactory; -function transformLoopExpr(expr: string, handlers: CustomHandlerSet) { - return `__$$evalArray(() => (${transformThis2Context(expr, handlers)}))`; -} - -function transformJsExpr(expr: string, handlers: CustomHandlerSet) { +function transformJsExpr(expr: string, scope: IScope) { if (!expr) { return 'undefined'; } @@ -188,14 +168,14 @@ function transformJsExpr(expr: string, handlers: CustomHandlerSet) { // 对于直接写个函数的,则不用再包下,因为这样不会抛出异常的 case 'ArrowFunctionExpression': case 'FunctionExpression': - return transformThis2Context(exprAst, handlers); + return transformThis2Context(exprAst, scope); default: break; } // 其他的都需要包一层 - return `__$$eval(() => (${transformThis2Context(exprAst, handlers)}))`; + return `__$$eval(() => (${transformThis2Context(exprAst, scope)}))`; } /** 判断是非是一些简单直接的字面值 */ @@ -243,41 +223,79 @@ function isLiteralAtomicExpr(expr: string): boolean { * 将所有的 this.xxx 替换为 __$$context.xxx * @param expr */ -function transformThis2Context(expr: string | Expression, customHandlers: CustomHandlerSet): string { +function transformThis2Context(expr: string | Expression, scope: IScope): string { // 下面这种字符串替换的方式虽然简单直接,但是对于复杂场景会误匹配,故后期改成了解析 AST 然后修改 AST 最后再重新生成代码的方式 // return expr // .replace(/\bthis\.item\./g, () => 'item.') // .replace(/\bthis\.index\./g, () => 'index.') // .replace(/\bthis\./g, () => '__$$context.'); - return parseExpressionConvertThis2Context(expr, '__$$context', customHandlers.scopeBindings?.getAllBindings() || []); + return parseExpressionConvertThis2Context(expr, '__$$context', scope.bindings?.getAllBindings() || []); } -function generateNodeAttrForRax(this: CustomHandlerSet, attrName: string, attrValue: CompositeValue): CodePiece[] { - if (!/^on/.test(attrName)) { - return generateAttr(attrName, attrValue, { - ...this, - nodeAttr: undefined, +function generateRaxLoopCtrl( + nodeItem: NodeSchema, + scope: IScope, + config?: NodeGeneratorConfig, + next?: NodePlugin, +): CodePiece[] { + if (nodeItem.loop) { + const loopItemName = nodeItem.loopArgs?.[0] || 'item'; + const loopIndexName = nodeItem.loopArgs?.[1] || 'index'; + const subScope = scope.createSubScope([loopItemName, loopIndexName]); + const pieces: CodePiece[] = next ? next(nodeItem, subScope, config) : []; + + const loopDataExpr = `__$$evalArray(() => (${transformThis2Context( + generateCompositeType(nodeItem.loop, scope, { handlers: config?.handlers }), + scope, + )}))`; + + pieces.unshift({ + value: `${loopDataExpr}.map((${loopItemName}, ${loopIndexName}) => (`, + type: PIECE_TYPE.BEFORE, }); + + pieces.push({ + value: '))', + type: PIECE_TYPE.AFTER, + }); + + return pieces; + } + + return next ? next(nodeItem, scope, config) : []; +} + +function generateNodeAttrForRax( + attrData: { attrName: string; attrValue: CompositeValue }, + scope: IScope, + config?: NodeGeneratorConfig, + next?: AttrPlugin, +): CodePiece[] { + if (!/^on/.test(attrData.attrName)) { + return next ? next(attrData, scope, config) : []; } // else: onXxx 的都是事件处理函数需要特殊处理下 - - return generateEventHandlerAttrForRax(attrName, attrValue, this); + return generateEventHandlerAttrForRax(attrData.attrName, attrData.attrValue, scope, config); } function generateEventHandlerAttrForRax( attrName: string, attrValue: CompositeValue, - handlers: CustomHandlerSet, + scope: IScope, + config?: NodeGeneratorConfig, ): CodePiece[] { // -- 事件处理函数中 JSExpression 转成 JSFunction 来处理,避免当 JSExpression 处理的时候多包一层 eval 而导致 Rax 转码成小程序的时候出问题 const valueExpr = generateCompositeType( isJSExpression(attrValue) ? { type: 'JSFunction', value: attrValue.value } : attrValue, - handlers, + scope, + { + handlers: config?.handlers, + }, ); // 查询当前作用域下的变量 - const currentScopeVariables = handlers.scopeBindings?.getAllBindings() || []; + const currentScopeVariables = scope.bindings?.getAllBindings() || []; if (currentScopeVariables.length <= 0) { return [ { @@ -322,31 +340,3 @@ function generateEventHandlerAttrForRax( }, ]; } - -function runInNewScope({ - scopeHost, - newScopeOwnVariables, - run, -}: { - scopeHost: { - scopeBindings?: IScopeBindings; - }; - newScopeOwnVariables: string[]; - run: () => T; -}): T { - const originalScopeBindings = scopeHost.scopeBindings; - - try { - const newScope = new ScopeBindings(originalScopeBindings); - - newScopeOwnVariables.forEach((varName) => { - newScope.addBinding(varName); - }); - - scopeHost.scopeBindings = newScope; - - return run(); - } finally { - scopeHost.scopeBindings = originalScopeBindings; - } -} diff --git a/packages/code-generator/src/plugins/component/react/containerDataSource.ts b/packages/code-generator/src/plugins/component/react/containerDataSource.ts index b60cb9539..84accc118 100644 --- a/packages/code-generator/src/plugins/component/react/containerDataSource.ts +++ b/packages/code-generator/src/plugins/component/react/containerDataSource.ts @@ -1,6 +1,7 @@ import { CLASS_DEFINE_CHUNK_NAME } from '../../../const/generator'; import { generateCompositeType } from '../../../utils/compositeType'; +import Scope from '../../../utils/Scope'; import { BuilderComponentPlugin, @@ -28,10 +29,12 @@ const pluginFactory: BuilderComponentPluginFactory = (config?) => const ir = next.ir as IContainerInfo; + const scope = Scope.createRootScope(); + if (ir.state) { const state = ir.state; const fields = Object.keys(state).map((stateName) => { - const value = generateCompositeType(state[stateName]); + const value = generateCompositeType(state[stateName], scope); return `${stateName}: ${value},`; }); diff --git a/packages/code-generator/src/plugins/component/react/containerInitState.ts b/packages/code-generator/src/plugins/component/react/containerInitState.ts index 9550244f1..503e7ed0f 100644 --- a/packages/code-generator/src/plugins/component/react/containerInitState.ts +++ b/packages/code-generator/src/plugins/component/react/containerInitState.ts @@ -1,6 +1,7 @@ import { CLASS_DEFINE_CHUNK_NAME, DEFAULT_LINK_AFTER } from '../../../const/generator'; import { generateCompositeType } from '../../../utils/compositeType'; +import Scope from '../../../utils/Scope'; import { BuilderComponentPlugin, @@ -29,11 +30,12 @@ const pluginFactory: BuilderComponentPluginFactory = (config?) => }; const ir = next.ir as IContainerInfo; + const scope = Scope.createRootScope(); if (ir.state) { const state = ir.state; const fields = Object.keys(state).map((stateName) => { - const value = generateCompositeType(state[stateName]); + const value = generateCompositeType(state[stateName], scope); return `${stateName}: ${value},`; }); diff --git a/packages/code-generator/src/plugins/component/recore/pageDataSource.ts b/packages/code-generator/src/plugins/component/recore/pageDataSource.ts index 898d1c99a..a071f65c9 100644 --- a/packages/code-generator/src/plugins/component/recore/pageDataSource.ts +++ b/packages/code-generator/src/plugins/component/recore/pageDataSource.ts @@ -1,6 +1,7 @@ import { JSExpression, CompositeValue } from '@ali/lowcode-types'; import { CLASS_DEFINE_CHUNK_NAME, DEFAULT_LINK_AFTER } from '../../../const/generator'; +import Scope from '../../../utils/Scope'; import { BuilderComponentPlugin, @@ -27,6 +28,8 @@ const pluginFactory: BuilderComponentPluginFactory = () => { }; const ir = next.ir as IContainerInfo; + const scope = Scope.createRootScope(); + if (ir.dataSource) { const { dataSource } = ir; const { list, ...rest } = dataSource; @@ -35,13 +38,13 @@ const pluginFactory: BuilderComponentPluginFactory = () => { const extConfigs = Object.keys(rest).map((extConfigName) => { const value = (rest as Record)[extConfigName]; - const valueStr = generateCompositeType(value); + const valueStr = generateCompositeType(value, scope); return `${extConfigName}: ${valueStr}`; }); attrs = [...attrs, ...extConfigs]; - const listProp = generateCompositeType((list as unknown) as CompositeValue, { + const listProp = generateCompositeType((list as unknown) as CompositeValue, scope, { handlers: { expression: packJsExpression, }, diff --git a/packages/code-generator/src/plugins/component/recore/pageVmBody.ts b/packages/code-generator/src/plugins/component/recore/pageVmBody.ts index 494d30362..e30327372 100644 --- a/packages/code-generator/src/plugins/component/recore/pageVmBody.ts +++ b/packages/code-generator/src/plugins/component/recore/pageVmBody.ts @@ -6,7 +6,7 @@ import { ChunkType, ICodeStruct, IContainerInfo, - INodeGeneratorContext, + IScope, CodePiece, PIECE_TYPE, } from '../../../types'; @@ -14,8 +14,9 @@ import { COMMON_CHUNK_NAME } from '../../../const/generator'; import { createNodeGenerator } from '../../../utils/nodeToJSX'; import { generateCompositeType } from '../../../utils/compositeType'; +import Scope from '../../../utils/Scope'; -const generateGlobalProps = (ctx: INodeGeneratorContext, nodeItem: NodeSchema): CodePiece[] => { +const generateGlobalProps = (nodeItem: NodeSchema): CodePiece[] => { return [ { value: `{...globalProps.${nodeItem.componentName}}`, @@ -24,11 +25,11 @@ const generateGlobalProps = (ctx: INodeGeneratorContext, nodeItem: NodeSchema): ]; }; -const generateCtrlLine = (ctx: INodeGeneratorContext, nodeItem: NodeSchema): CodePiece[] => { +const generateCtrlLine = (nodeItem: NodeSchema, scope: IScope): CodePiece[] => { const pieces: CodePiece[] = []; if (nodeItem.loop && nodeItem.loopArgs) { - const loopDataExp = generateCompositeType(nodeItem.loop); + const loopDataExp = generateCompositeType(nodeItem.loop, scope); pieces.push({ type: PIECE_TYPE.ATTR, value: `x-for={${loopDataExp}}`, @@ -41,7 +42,7 @@ const generateCtrlLine = (ctx: INodeGeneratorContext, nodeItem: NodeSchema): Cod } if (nodeItem.condition) { - const conditionExp = generateCompositeType(nodeItem.condition); + const conditionExp = generateCompositeType(nodeItem.condition, scope); pieces.push({ type: PIECE_TYPE.ATTR, value: `x-if={${conditionExp}}`, @@ -53,7 +54,7 @@ const generateCtrlLine = (ctx: INodeGeneratorContext, nodeItem: NodeSchema): Cod const pluginFactory: BuilderComponentPluginFactory = () => { const generator = createNodeGenerator({ - plugins: [generateGlobalProps, generateCtrlLine], + nodePlugins: [generateGlobalProps, generateCtrlLine], }); const plugin: BuilderComponentPlugin = async (pre: ICodeStruct) => { @@ -62,8 +63,9 @@ const pluginFactory: BuilderComponentPluginFactory = () => { }; const ir = next.ir as IContainerInfo; + const scope = Scope.createRootScope(); - const vxContent = generator(ir); + const vxContent = generator(ir, scope); next.chunks.push({ type: ChunkType.STRING, diff --git a/packages/code-generator/src/plugins/project/constants.ts b/packages/code-generator/src/plugins/project/constants.ts index 9916e0bcf..08870e507 100644 --- a/packages/code-generator/src/plugins/project/constants.ts +++ b/packages/code-generator/src/plugins/project/constants.ts @@ -8,6 +8,7 @@ import { ICodeStruct, IProjectInfo, } from '../../types'; +import Scope from '../../utils/Scope'; const pluginFactory: BuilderComponentPluginFactory = () => { const plugin: BuilderComponentPlugin = async (pre: ICodeStruct) => { @@ -16,7 +17,8 @@ const pluginFactory: BuilderComponentPluginFactory = () => { }; const ir = next.ir as IProjectInfo; - const constantStr = generateCompositeType(ir.constants || {}); + const scope = Scope.createRootScope(); + const constantStr = generateCompositeType(ir.constants || {}, scope); next.chunks.push({ type: ChunkType.STRING, diff --git a/packages/code-generator/src/types/core.ts b/packages/code-generator/src/types/core.ts index 93d069a9e..8666f507e 100644 --- a/packages/code-generator/src/types/core.ts +++ b/packages/code-generator/src/types/core.ts @@ -1,13 +1,11 @@ import { JSONArray, JSONObject, - CompositeValue, CompositeArray, CompositeObject, ResultDir, ResultFile, NodeDataType, - NodeSchema, ProjectSchema, JSExpression, JSFunction, @@ -15,6 +13,7 @@ import { } from '@ali/lowcode-types'; import { IParseResult } from './intermediate'; +import { IScopeBindings } from '../utils/ScopeBindings'; export enum FileType { CSS = 'css', @@ -137,59 +136,35 @@ export interface IPluginOptions { fileDirDepth: number; } -export enum PIECE_TYPE { - BEFORE = 'NodeCodePieceBefore', - TAG = 'NodeCodePieceTag', - ATTR = 'NodeCodePieceAttr', - CHILDREN = 'NodeCodePieceChildren', - AFTER = 'NodeCodePieceAfter', -} +export type BaseGenerator = (input: I, scope: IScope, config?: C, next?: BaseGenerator) => T; +type CompositeTypeGenerator = + | BaseGenerator + | Array>; -export interface CodePiece { - value: string; - type: PIECE_TYPE; -} +export type NodeGenerator = (nodeItem: NodeDataType, scope: IScope) => T; // FIXME: 在新的实现中,添加了第一参数 this: CustomHandlerSet 作为上下文。究其本质 // scopeBindings?: IScopeBindings; +// 这个组合只用来用来处理 CompositeValue 类型,不是这个类型的不要放在这里 export interface HandlerSet { - string?: (input: string) => T; - boolean?: (input: boolean) => T; - number?: (input: number) => T; - expression?: (input: JSExpression) => T; - function?: (input: JSFunction) => T; - slot?: (input: JSSlot) => T; - node?: (input: NodeSchema) => T; - array?: (input: JSONArray | CompositeArray) => T; - children?: (input: T[]) => T; - object?: (input: JSONObject | CompositeObject) => T; - common?: (input: unknown) => T; - tagName?: (input: string) => T; - loopDataExpr?: (input: string) => T; - conditionExpr?: (input: string) => T; - nodeAttrs?(node: NodeSchema): CodePiece[]; - nodeAttr?(attrName: string, attrValue: CompositeValue): CodePiece[]; + string?: CompositeTypeGenerator; + boolean?: CompositeTypeGenerator; + number?: CompositeTypeGenerator; + expression?: CompositeTypeGenerator; + function?: CompositeTypeGenerator; + slot?: CompositeTypeGenerator; + array?: CompositeTypeGenerator; + object?: CompositeTypeGenerator; } -export type ExtGeneratorPlugin = (ctx: INodeGeneratorContext, nodeItem: NodeSchema) => CodePiece[]; - -export type NodeGeneratorConfig = { - handlers?: HandlerSet; - plugins?: ExtGeneratorPlugin[]; -}; - -export type NodeGenerator = (nodeItem: NodeDataType) => string; - -export interface INodeGeneratorContext { - handlers: HandlerSet; - plugins: ExtGeneratorPlugin[]; - generator: NodeGenerator; -} - -export type CompositeValueCustomHandler = (data: unknown) => string; - export type CompositeValueGeneratorOptions = { handlers?: HandlerSet; - containerHandler?: (value: string, isString: boolean, valStr: string) => string; - nodeGenerator?: NodeGenerator; + nodeGenerator?: NodeGenerator; }; + +export interface IScope { + // 作用域内定义 + bindings?: IScopeBindings; + // TODO: 需要有上下文信息吗? 描述什么内容 + createSubScope: (ownIndentifiers: string[]) => IScope; +} diff --git a/packages/code-generator/src/types/index.ts b/packages/code-generator/src/types/index.ts index c7ad3e521..92ca81924 100644 --- a/packages/code-generator/src/types/index.ts +++ b/packages/code-generator/src/types/index.ts @@ -3,3 +3,4 @@ export * from './deps'; export * from './error'; export * from './intermediate'; export * from './publisher'; +export * from './jsx'; diff --git a/packages/code-generator/src/types/jsx.ts b/packages/code-generator/src/types/jsx.ts new file mode 100644 index 000000000..6ec5c7fef --- /dev/null +++ b/packages/code-generator/src/types/jsx.ts @@ -0,0 +1,29 @@ +import { NodeSchema, CompositeValue } from '@ali/lowcode-types'; +import { HandlerSet, BaseGenerator, NodeGenerator } from './core'; + +export enum PIECE_TYPE { + BEFORE = 'NodeCodePieceBefore', + TAG = 'NodeCodePieceTag', + ATTR = 'NodeCodePieceAttr', + CHILDREN = 'NodeCodePieceChildren', + AFTER = 'NodeCodePieceAfter', +} + +export interface CodePiece { + name?: string; + value: string; + type: PIECE_TYPE; +} + +export type AttrData = { attrName: string; attrValue: CompositeValue }; +// 对 JSX 出码的理解,目前定制点包含 【包装】【标签名】【属性】 +export type AttrPlugin = BaseGenerator; +export type NodePlugin = BaseGenerator; + +export type NodeGeneratorConfig = { + handlers?: HandlerSet; + tagMapping?: (input: string) => string; + attrPlugins?: AttrPlugin[]; + nodePlugins?: NodePlugin[]; + self?: NodeGenerator; +}; diff --git a/packages/code-generator/src/utils/Scope.ts b/packages/code-generator/src/utils/Scope.ts new file mode 100644 index 000000000..e8b7bf235 --- /dev/null +++ b/packages/code-generator/src/utils/Scope.ts @@ -0,0 +1,27 @@ +import { IScope } from '../types/core'; +import { IScopeBindings, ScopeBindings } from './ScopeBindings'; + +class Scope implements IScope { + static createRootScope(): IScope { + return new Scope(); + } + + bindings?: IScopeBindings; + + constructor() { + this.bindings = undefined; + } + + createSubScope(ownIndentifiers: string[]): IScope { + const originalScopeBindings = this.bindings; + const newScopeBindings = new ScopeBindings(originalScopeBindings); + ownIndentifiers.forEach((identifier) => { + newScopeBindings.addBinding(identifier); + }); + const newScope = new Scope(); + newScope.bindings = newScopeBindings; + return newScope; + } +} + +export default Scope; diff --git a/packages/code-generator/src/utils/aopHelper.ts b/packages/code-generator/src/utils/aopHelper.ts new file mode 100644 index 000000000..5c4478c3d --- /dev/null +++ b/packages/code-generator/src/utils/aopHelper.ts @@ -0,0 +1,27 @@ +import { BaseGenerator, IScope } from '../types/core'; + +export function executeFunctionStack( + input: I, + scope: IScope, + funcs: BaseGenerator | Array>, + baseFunc: BaseGenerator, + config?: C, +): T { + const funcList: Array> = []; + if (Array.isArray(funcs)) { + funcList.push(...funcs); + } else { + funcList.push(funcs); + } + + let next: BaseGenerator = baseFunc; + while (funcList.length > 0) { + const func = funcList.pop(); + if (func) { + const warppedFunc = ((nextFunc) => (input: I, scope: IScope, cfg?: C) => func(input, scope, cfg, nextFunc))(next); + next = warppedFunc; + } + } + + return next(input, scope, config); +} diff --git a/packages/code-generator/src/utils/compositeType.ts b/packages/code-generator/src/utils/compositeType.ts index 8471e240f..01b953fc3 100644 --- a/packages/code-generator/src/utils/compositeType.ts +++ b/packages/code-generator/src/utils/compositeType.ts @@ -2,24 +2,27 @@ import { CompositeArray, CompositeValue, CompositeObject, + JSFunction, isJSExpression, isJSFunction, isJSSlot, + JSSlot, } from '@ali/lowcode-types'; import _ from 'lodash'; -import { CompositeValueGeneratorOptions, CodeGeneratorError } from '../types'; +import { IScope, CompositeValueGeneratorOptions, CodeGeneratorError } from '../types'; import { generateExpression, generateFunction } from './jsExpression'; import { generateJsSlot } from './jsSlot'; import { isValidIdentifier } from './validate'; import { camelize } from './common'; +import { executeFunctionStack } from './aopHelper'; -function generateArray(value: CompositeArray, options: CompositeValueGeneratorOptions = {}): string { - const body = value.map((v) => generateUnknownType(v, options)).join(','); +function generateArray(value: CompositeArray, scope: IScope, options: CompositeValueGeneratorOptions = {}): string { + const body = value.map((v) => generateUnknownType(v, scope, options)).join(','); return `[${body}]`; } -function generateObject(value: CompositeObject, options: CompositeValueGeneratorOptions = {}): string { +function generateObject(value: CompositeObject, scope: IScope, options: CompositeValueGeneratorOptions = {}): string { const body = Object.keys(value) .map((key) => { let propName = key; @@ -39,7 +42,7 @@ function generateObject(value: CompositeObject, options: CompositeValueGenerator throw new CodeGeneratorError(`Propname: ${key} is not a valid identifier.`); } } - const v = generateUnknownType(value[key], options); + const v = generateUnknownType(value[key], scope, options); return `${propName}: ${v}`; }) .join(',\n'); @@ -47,7 +50,34 @@ function generateObject(value: CompositeObject, options: CompositeValueGenerator return `{${body}}`; } -function generateUnknownType(value: CompositeValue, options: CompositeValueGeneratorOptions = {}): string { +function generateString(value: string): string { + return `'${value}'`; +} + +function generateNumber(value: number): string { + return String(value); +} + +function generateBool(value: boolean): string { + return value ? 'true' : 'false'; +} + +function genFunction(value: JSFunction): string { + return generateFunction(value, { isArrow: true }); +} + +function genJsSlot(value: JSSlot, scope: IScope, options: CompositeValueGeneratorOptions = {}) { + if (options.nodeGenerator) { + return generateJsSlot(value, scope, options.nodeGenerator); + } + return ''; +} + +function generateUnknownType( + value: CompositeValue, + scope: IScope, + options: CompositeValueGeneratorOptions = {}, +): string { if (_.isUndefined(value)) { return 'undefined'; } @@ -58,67 +88,70 @@ function generateUnknownType(value: CompositeValue, options: CompositeValueGener if (_.isArray(value)) { if (options.handlers?.array) { - return options.handlers.array(value); + return executeFunctionStack(value, scope, options.handlers.array, generateArray, options); } - return generateArray(value, options); + return generateArray(value, scope, options); } if (isJSExpression(value)) { if (options.handlers?.expression) { - return options.handlers.expression(value); + return executeFunctionStack(value, scope, options.handlers.expression, generateExpression, options); } return generateExpression(value); } if (isJSFunction(value)) { if (options.handlers?.function) { - return options.handlers.function(value); + return executeFunctionStack(value, scope, options.handlers.function, genFunction, options); } - return generateFunction(value, { isArrow: true }); + return genFunction(value); } if (isJSSlot(value)) { - if (options.nodeGenerator) { - return generateJsSlot(value, options.nodeGenerator); + if (options.handlers?.slot) { + return executeFunctionStack(value, scope, options.handlers.slot, genJsSlot, options); } - throw new CodeGeneratorError("Can't find Node Generator"); + return genJsSlot(value, scope, options); } if (_.isObject(value)) { if (options.handlers?.object) { - return options.handlers.object(value); + return executeFunctionStack(value, scope, options.handlers.object, generateObject, options); } - return generateObject(value as CompositeObject, options); + return generateObject(value as CompositeObject, scope, options); } if (_.isString(value)) { if (options.handlers?.string) { - return options.handlers.string(value); + return executeFunctionStack(value, scope, options.handlers.string, generateString, options); } - return `'${value}'`; + return generateString(value); } - if (_.isNumber(value) && options.handlers?.number) { - return options.handlers.number(value); + if (_.isNumber(value)) { + if (options.handlers?.number) { + return executeFunctionStack(value, scope, options.handlers.number, generateNumber, options); + } + return generateNumber(value); } - if (_.isBoolean(value) && options.handlers?.boolean) { - return options.handlers.boolean(value); + if (_.isBoolean(value)) { + if (options.handlers?.boolean) { + return executeFunctionStack(value, scope, options.handlers.boolean, generateBool, options); + } + return generateBool(value); } - return JSON.stringify(value); + throw new CodeGeneratorError('Meet unknown composite value type'); } -const defaultContainer = (v: string) => v; - -export function generateCompositeType(value: CompositeValue, options: CompositeValueGeneratorOptions = {}): string { - const isStringType = _.isString(value); - const result = generateUnknownType(value, options); - const handler = options.containerHandler || defaultContainer; - - if (isStringType && result.length >= 2) { - return handler(result, true, result.substring(1, result.length - 1)); - } - - return handler(result, false, result); +// 这一层曾经是对产出做最外层包装的,但其实包装逻辑不应该属于这一层 +// 这一层先不去掉,做冗余,方便后续重构 +export function generateCompositeType( + value: CompositeValue, + scope: IScope, + options: CompositeValueGeneratorOptions = {}, +): string { + const result = generateUnknownType(value, scope, options); + return result; } diff --git a/packages/code-generator/src/utils/jsSlot.ts b/packages/code-generator/src/utils/jsSlot.ts index 2cfb18436..b1d9c34bc 100644 --- a/packages/code-generator/src/utils/jsSlot.ts +++ b/packages/code-generator/src/utils/jsSlot.ts @@ -1,5 +1,5 @@ import { JSSlot, isJSSlot } from '@ali/lowcode-types'; -import { CodeGeneratorError, NodeGenerator } from '../types'; +import { CodeGeneratorError, NodeGenerator, IScope } from '../types'; function generateSingleLineComment(commentText: string): string { return ( @@ -12,23 +12,23 @@ function generateSingleLineComment(commentText: string): string { ); } -export function generateJsSlot(slot: any, generator: NodeGenerator): string { +export function generateJsSlot(slot: any, scope: IScope, generator: NodeGenerator): string { if (isJSSlot(slot)) { const { title, params, value } = slot as JSSlot; - if (!value) { - return 'null'; + if (params) { + return [ + title && generateSingleLineComment(title), + `(`, + ...(params || []), + `) => (`, + !value ? 'null' : generator(value, scope), + `)`, + ] + .filter(Boolean) + .join(''); } - return [ - title && generateSingleLineComment(title), - `(`, - ...(params || []), - `) => (`, - ...(!value ? ['null'] : !Array.isArray(value) ? [generator(value)] : value.map((node) => generator(node))), - `)`, - ] - .filter(Boolean) - .join(''); + return !value ? 'null' : generator(value, scope); } throw new CodeGeneratorError('Not a JSSlot'); diff --git a/packages/code-generator/src/utils/nodeToJSX.ts b/packages/code-generator/src/utils/nodeToJSX.ts index 3f454d076..a93ec64d2 100644 --- a/packages/code-generator/src/utils/nodeToJSX.ts +++ b/packages/code-generator/src/utils/nodeToJSX.ts @@ -1,113 +1,105 @@ import _ from 'lodash'; -import { - JSSlot, - JSExpression, - NodeData, - NodeSchema, - isNodeSchema, - PropsMap, - isJSExpression, - isJSSlot, - isDOMText, - NodeDataType, -} from '@ali/lowcode-types'; +import { NodeSchema, isNodeSchema, NodeDataType, CompositeValue } from '@ali/lowcode-types'; import { + IScope, CodeGeneratorError, PIECE_TYPE, CodePiece, - HandlerSet, NodeGenerator, - INodeGeneratorContext, NodeGeneratorConfig, + NodePlugin, + AttrData, } from '../types'; import { generateCompositeType } from './compositeType'; +import { executeFunctionStack } from './aopHelper'; -// tslint:disable-next-line: no-empty -const noop = () => undefined; - -const handleChildrenDefaultOptions = { - rerun: false, -}; - -export function handleSubNodes( - children: unknown, - handlers: HandlerSet, - options?: { - rerun?: boolean; - }, -): T[] { - const opt = { - ...handleChildrenDefaultOptions, - ...(options || {}), - }; - - if (Array.isArray(children)) { - const list: NodeData[] = children as NodeData[]; - return list.map((child) => handleSubNodes(child, handlers, opt)).reduce((p, c) => p.concat(c), []); +function mergeNodeGeneratorConfig(cfg1: NodeGeneratorConfig, cfg2: NodeGeneratorConfig = {}): NodeGeneratorConfig { + const resCfg: NodeGeneratorConfig = {}; + if (cfg1.handlers || cfg2.handlers) { + resCfg.handlers = { + ...(cfg1.handlers || {}), + ...(cfg2.handlers || {}), + }; } - let result: T | undefined = undefined; - const childrenRes: T[] = []; - if (isDOMText(children)) { - const handler = handlers.string || handlers.common || noop; - result = handler(children as string); - } else if (isJSExpression(children)) { - const handler = handlers.expression || handlers.common || noop; - result = handler(children as JSExpression); - } else { - const handler = handlers.node || handlers.common || noop; - const child = children as NodeSchema; - result = handler(child); - - if (opt.rerun && child.children) { - const childRes = handleSubNodes(child.children, handlers, opt); - childrenRes.push(...childRes); - } - if (child.props) { - // FIXME: currently only support PropsMap - const childProps = child.props as PropsMap; - Object.keys(childProps) - .filter((propName) => isJSSlot(childProps[propName])) - .forEach((propName) => { - const soltVals = (childProps[propName] as JSSlot).value; - const childRes = handleSubNodes(soltVals, handlers, opt); - childrenRes.push(...childRes); - }); - } + if (cfg1.tagMapping || cfg2.tagMapping) { + resCfg.tagMapping = cfg2.tagMapping || cfg1.tagMapping; } - if (result !== undefined) { - childrenRes.unshift(result); + if (cfg1.attrPlugins || cfg2.attrPlugins) { + resCfg.attrPlugins = []; + resCfg.attrPlugins.push(...(cfg2.attrPlugins || [])); + resCfg.attrPlugins.push(...(cfg1.attrPlugins || [])); } - return childrenRes; + if (cfg1.nodePlugins || cfg2.nodePlugins) { + resCfg.nodePlugins = []; + resCfg.nodePlugins.push(...(cfg2.nodePlugins || [])); + resCfg.nodePlugins.push(...(cfg1.nodePlugins || [])); + } + + return resCfg; } -export function generateAttr(ctx: INodeGeneratorContext, attrName: string, attrValue: any): CodePiece[] { - if (ctx.handlers.nodeAttr) { - return ctx.handlers.nodeAttr(attrName, attrValue); - } - - const valueStr = generateCompositeType(attrValue, { - containerHandler: (v, isStr, vStr) => (isStr ? `"${vStr}"` : `{${v}}`), - nodeGenerator: ctx.generator, +function generateAttrValue( + attrData: { attrName: string; attrValue: CompositeValue }, + scope: IScope, + config?: NodeGeneratorConfig, +): CodePiece[] { + const valueStr = generateCompositeType(attrData.attrValue, scope, { + handlers: config?.handlers, + nodeGenerator: config?.self, }); - return [ { - value: `${attrName}=${valueStr}`, type: PIECE_TYPE.ATTR, + name: attrData.attrName, + value: valueStr, }, ]; } -export function generateAttrs(ctx: INodeGeneratorContext, nodeItem: NodeSchema): CodePiece[] { - if (ctx.handlers.nodeAttrs) { - return ctx.handlers.nodeAttrs(nodeItem); +function generateAttr( + attrName: string, + attrValue: CompositeValue, + scope: IScope, + config?: NodeGeneratorConfig, +): CodePiece[] { + let pieces: CodePiece[]; + if (config?.attrPlugins) { + pieces = executeFunctionStack( + { attrName, attrValue }, + scope, + config.attrPlugins, + generateAttrValue, + config, + ); + } else { + pieces = generateAttrValue({ attrName, attrValue }, scope, config); } + pieces = pieces.map((p) => { + // FIXME: 在经过 generateCompositeType 处理过之后,其实已经无法通过传入值的类型判断传出值是否为纯字面值字符串了(可能包裹了加工函数之类的) + // 因此这个处理最好的方式是对传出值做语法分析,判断以哪种模版产出 Attr 值 + let newValue: string; + if (p.value && p.value[0] === "'" && p.value[p.value.length - 1] === "'") { + newValue = `"${p.value.substring(1, p.value.length - 1)}"`; + } else { + newValue = `{${p.value}}`; + } + + return { + value: `${p.name}=${newValue}`, + type: PIECE_TYPE.ATTR, + }; + }); + + return pieces; +} + +function generateAttrs(nodeItem: NodeSchema, scope: IScope, config?: NodeGeneratorConfig): CodePiece[] { const { props } = nodeItem; let pieces: CodePiece[] = []; @@ -115,12 +107,12 @@ export function generateAttrs(ctx: INodeGeneratorContext, nodeItem: NodeSchema): if (props) { if (!Array.isArray(props)) { Object.keys(props).forEach((propName: string) => { - pieces = pieces.concat(generateAttr(ctx, propName, props[propName])); + pieces = pieces.concat(generateAttr(propName, props[propName], scope, config)); }); } else { props.forEach((prop) => { if (prop.name && !prop.spread) { - pieces = pieces.concat(generateAttr(ctx, prop.name, prop.value)); + pieces = pieces.concat(generateAttr(prop.name, prop.value, scope, config)); } // TODO: 处理 spread 场景() @@ -132,9 +124,9 @@ export function generateAttrs(ctx: INodeGeneratorContext, nodeItem: NodeSchema): return pieces; } -export function generateBasicNode(ctx: INodeGeneratorContext, nodeItem: NodeSchema): CodePiece[] { +function generateBasicNode(nodeItem: NodeSchema, scope: IScope, config?: NodeGeneratorConfig): CodePiece[] { const pieces: CodePiece[] = []; - const tagName = (ctx.handlers.tagName || _.identity)(nodeItem.componentName); + const tagName = (config?.tagMapping || _.identity)(nodeItem.componentName); pieces.push({ value: tagName || '', // FIXME: type detection error @@ -144,7 +136,14 @@ export function generateBasicNode(ctx: INodeGeneratorContext, nodeItem: NodeSche return pieces; } -export function linkPieces(pieces: CodePiece[]): string { +function generateSimpleNode(nodeItem: NodeSchema, scope: IScope, config?: NodeGeneratorConfig): CodePiece[] { + const basicParts = generateBasicNode(nodeItem, scope, config) || []; + const attrParts = generateAttrs(nodeItem, scope, config) || []; + + return [...basicParts, ...attrParts]; +} + +function linkPieces(pieces: CodePiece[]): string { const tagsPieces = pieces.filter((p) => p.type === PIECE_TYPE.TAG); if (tagsPieces.length !== 1) { throw new CodeGeneratorError('One node only need one tag define'); @@ -180,17 +179,23 @@ export function linkPieces(pieces: CodePiece[]): string { return `${beforeParts}<${tagName}${attrsParts} />${afterParts}`; } -export function generateNodeSchema(nodeItem: NodeSchema, ctx: INodeGeneratorContext): string { - let pieces: CodePiece[] = []; +function generateNodeSchema(nodeItem: NodeSchema, scope: IScope, config?: NodeGeneratorConfig): string { + const pieces: CodePiece[] = []; + if (config?.nodePlugins) { + const res = executeFunctionStack( + nodeItem, + scope, + config.nodePlugins, + generateSimpleNode, + config, + ); + pieces.push(...res); + } else { + pieces.push(...generateSimpleNode(nodeItem, scope, config)); + } - ctx.plugins.forEach((p) => { - pieces = pieces.concat(p(ctx, nodeItem)); - }); - pieces = pieces.concat(generateBasicNode(ctx, nodeItem)); - pieces = pieces.concat(generateAttrs(ctx, nodeItem)); - - if (nodeItem.children) { - const childrenStr = ctx.generator(nodeItem.children); + if (nodeItem.children && config?.self) { + const childrenStr = config.self(nodeItem.children, scope); pieces.push({ type: PIECE_TYPE.CHILDREN, @@ -207,25 +212,28 @@ export function generateNodeSchema(nodeItem: NodeSchema, ctx: INodeGeneratorCont // export function createSubContext() {} /** - * JSX 生成逻辑插件。在 React 代码模式下生成 loop 与 condition 相关的逻辑代码 - * @type ExtGeneratorPlugin + * JSX 生成逻辑插件。在 React 代码模式下生成 loop 相关的逻辑代码 + * @type NodePlugin Extended * * @export * @param {NodeSchema} nodeItem 当前 UI 节点 * @returns {CodePiece[]} 实现功能的相关代码片段 */ -export function generateReactCtrlLine(ctx: INodeGeneratorContext, nodeItem: NodeSchema): CodePiece[] { - const pieces: CodePiece[] = []; +export function generateReactLoopCtrl( + nodeItem: NodeSchema, + scope: IScope, + config?: NodeGeneratorConfig, + next?: NodePlugin, +): CodePiece[] { + const pieces: CodePiece[] = next ? next(nodeItem, scope, config) : []; if (nodeItem.loop) { const loopItemName = nodeItem.loopArgs?.[0] || 'item'; const loopIndexName = nodeItem.loopArgs?.[1] || 'index'; - const rawLoopDataExpr = isJSExpression(nodeItem.loop) - ? `(${nodeItem.loop.value})` - : `(${JSON.stringify(nodeItem.loop)})`; - - const loopDataExpr = ctx.handlers.loopDataExpr ? ctx.handlers.loopDataExpr(rawLoopDataExpr) : rawLoopDataExpr; + const loopDataExpr = generateCompositeType(nodeItem.loop, scope, { + handlers: config?.handlers, + }); pieces.unshift({ value: `${loopDataExpr}.map((${loopItemName}, ${loopIndexName}) => (`, @@ -238,15 +246,32 @@ export function generateReactCtrlLine(ctx: INodeGeneratorContext, nodeItem: Node }); } + return pieces; +} + +/** + * JSX 生成逻辑插件。在 React 代码模式下生成 condition 相关的逻辑代码 + * @type NodePlugin + * + * @export + * @param {NodeSchema} nodeItem 当前 UI 节点 + * @returns {CodePiece[]} 实现功能的相关代码片段 + */ +export function generateConditionReactCtrl( + nodeItem: NodeSchema, + scope: IScope, + config?: NodeGeneratorConfig, + next?: NodePlugin, +): CodePiece[] { + const pieces: CodePiece[] = next ? next(nodeItem, scope, config) : []; + if (nodeItem.condition) { - const value = generateCompositeType(nodeItem.condition, { - nodeGenerator: ctx.generator, + const value = generateCompositeType(nodeItem.condition, scope, { + handlers: config?.handlers, }); - const conditionExpr = (ctx.handlers.conditionExpr || _.identity)(value); - pieces.unshift({ - value: `(${conditionExpr}) && (`, + value: `(${value}) && (`, type: PIECE_TYPE.BEFORE, }); @@ -256,6 +281,25 @@ export function generateReactCtrlLine(ctx: INodeGeneratorContext, nodeItem: Node }); } + return pieces; +} + +/** + * JSX 生成逻辑插件。在 React 代码模式下,如果 Node 生成结果是一个表达式,则对其进行 { Expression } 包装 + * @type NodePlugin + * + * @export + * @param {NodeSchema} nodeItem 当前 UI 节点 + * @returns {CodePiece[]} 实现功能的相关代码片段 + */ +export function generateReactExprInJS( + nodeItem: NodeSchema, + scope: IScope, + config?: NodeGeneratorConfig, + next?: NodePlugin, +): CodePiece[] { + const pieces: CodePiece[] = next ? next(nodeItem, scope, config) : []; + if (nodeItem.condition || nodeItem.loop) { pieces.unshift({ value: '{', @@ -273,45 +317,35 @@ export function generateReactCtrlLine(ctx: INodeGeneratorContext, nodeItem: Node const handleChildren = (v: string[]) => v.join(''); -export function createNodeGenerator(cfg: NodeGeneratorConfig = {}): NodeGenerator { - let ctx: INodeGeneratorContext = { handlers: {}, plugins: [], generator: () => '' }; - - const generateNode = (nodeItem: NodeDataType): string => { +export function createNodeGenerator(cfg: NodeGeneratorConfig = {}): NodeGenerator { + const generateNode = (nodeItem: NodeDataType, scope: IScope): string => { if (_.isArray(nodeItem)) { - const resList = nodeItem.map((n) => generateNode(n)); - return (cfg?.handlers?.children || handleChildren)(resList); + const resList = nodeItem.map((n) => generateNode(n, scope)); + return handleChildren(resList); } if (isNodeSchema(nodeItem)) { - if (cfg?.handlers?.node) { - // TODO: children 的处理是否拆出来作为公用 - return cfg.handlers.node(nodeItem); - } - - return generateNodeSchema(nodeItem, ctx); + return generateNodeSchema(nodeItem, scope, { + ...cfg, + self: generateNode, + }); } - return generateCompositeType(nodeItem, { + return generateCompositeType(nodeItem, scope, { handlers: cfg.handlers, - // FIXME: 这里和 children 类型的嵌套逻辑需要再思考一下 - // containerHandler: (value: string, isString: boolean, valStr: string) => (isString ? valStr : value), nodeGenerator: generateNode, }); }; - ctx = { - handlers: cfg?.handlers || {}, - plugins: cfg.plugins || [], - generator: generateNode, - }; - return generateNode; } -// TODO: 需要一个 merge config 的方法。 -export function createReactNodeGenerator(cfg?: NodeGeneratorConfig): NodeGenerator { - return createNodeGenerator({ - plugins: [generateReactCtrlLine], - ...cfg, - }); +const defaultReactGeneratorConfig: NodeGeneratorConfig = { + nodePlugins: [generateReactExprInJS, generateConditionReactCtrl, generateReactLoopCtrl], +}; + +export function createReactNodeGenerator(cfg?: NodeGeneratorConfig): NodeGenerator { + const newCfg = mergeNodeGeneratorConfig(defaultReactGeneratorConfig, cfg); + + return createNodeGenerator(newCfg); } diff --git a/packages/code-generator/src/utils/schema.ts b/packages/code-generator/src/utils/schema.ts index f741bb111..373de303e 100644 --- a/packages/code-generator/src/utils/schema.ts +++ b/packages/code-generator/src/utils/schema.ts @@ -1,4 +1,15 @@ -import { ContainerSchema, NpmInfo } from '@ali/lowcode-types'; +import { + JSSlot, + JSExpression, + NodeData, + NodeSchema, + PropsMap, + isJSExpression, + isJSSlot, + isDOMText, + ContainerSchema, + NpmInfo, +} from '@ali/lowcode-types'; export function isContainerSchema(x: any): x is ContainerSchema { return typeof x === 'object' && x && typeof x.componentName === 'string' && typeof x.fileName === 'string'; @@ -7,3 +18,68 @@ export function isContainerSchema(x: any): x is ContainerSchema { export function isNpmInfo(x: any): x is NpmInfo { return typeof x === 'object' && x && typeof x.package === 'string'; } + +// tslint:disable-next-line: no-empty +const noop = () => undefined; + +const handleChildrenDefaultOptions = { + rerun: false, +}; + +export function handleSubNodes( + children: unknown, + handlers: { + string?: (i: string) => T; + expression?: (i: JSExpression) => T; + node?: (i: NodeSchema) => T; + }, + options?: { + rerun?: boolean; + }, +): T[] { + const opt = { + ...handleChildrenDefaultOptions, + ...(options || {}), + }; + + if (Array.isArray(children)) { + const list: NodeData[] = children as NodeData[]; + return list.map((child) => handleSubNodes(child, handlers, opt)).reduce((p, c) => p.concat(c), []); + } + + let result: T | undefined = undefined; + const childrenRes: T[] = []; + if (isDOMText(children)) { + const handler = handlers.string || noop; + result = handler(children as string); + } else if (isJSExpression(children)) { + const handler = handlers.expression || noop; + result = handler(children as JSExpression); + } else { + const handler = handlers.node || noop; + const child = children as NodeSchema; + result = handler(child); + + if (opt.rerun && child.children) { + const childRes = handleSubNodes(child.children, handlers, opt); + childrenRes.push(...childRes); + } + if (child.props) { + // FIXME: currently only support PropsMap + const childProps = child.props as PropsMap; + Object.keys(childProps) + .filter((propName) => isJSSlot(childProps[propName])) + .forEach((propName) => { + const soltVals = (childProps[propName] as JSSlot).value; + const childRes = handleSubNodes(soltVals, handlers, opt); + childrenRes.push(...childRes); + }); + } + } + + if (result !== undefined) { + childrenRes.unshift(result); + } + + return childrenRes; +}