refactor: 💡 support scope info & use middleware style plugin

This commit is contained in:
春希 2020-09-12 02:30:18 +08:00
parent fe327f4e19
commit ea9d71f81a
18 changed files with 541 additions and 331 deletions

View File

@ -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 {

View File

@ -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<PluginConfig> = (config?) =>
};
const ir = next.ir as IContainerInfo;
const scope = Scope.createRootScope();
if (ir.state) {
const state = ir.state;
const fields = Object.keys(state).map<string>((stateName) => {
// TODO: 这里用什么 handlers?
const value = generateCompositeType(state[stateName], {});
const value = generateCompositeType(state[stateName], scope);
return `${stateName}: ${value}`;
});

View File

@ -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<PluginConfig> = (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<PluginConfig> = (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)}))}`,
};
}

View File

@ -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<PluginConfig> = (config?) =>
};
const ir = next.ir as IContainerInfo;
const rootScope = Scope.createRootScope();
// Rax 构建到小程序的时候,不能给组件起起别名,得直接引用,故这里将所有的别名替换掉
// 先收集下所有的 alias 的映射
@ -70,44 +74,24 @@ const pluginFactory: BuilderComponentPluginFactory<PluginConfig> = (config?) =>
// 2. 小程序出码的时候,很容易出现 Uncaught TypeError: Cannot read property 'avatar' of undefined 这样的异常(如下图的 50 行) -- 因为若直接出码Rax 构建到小程序的时候会立即计算所有在视图中用到的变量
// 3. 通过 this.xxx 能拿到的东西太多了,而且自定义的 methods 可能会无意间破坏 Rax 框架或小程序框架在页面 this 上的东东
const customHandlers: HandlerSet<string> = {
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<PluginConfig> = (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<T>({
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;
}
}

View File

@ -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<PluginConfig> = (config?) =>
const ir = next.ir as IContainerInfo;
const scope = Scope.createRootScope();
if (ir.state) {
const state = ir.state;
const fields = Object.keys(state).map<string>((stateName) => {
const value = generateCompositeType(state[stateName]);
const value = generateCompositeType(state[stateName], scope);
return `${stateName}: ${value},`;
});

View File

@ -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<PluginConfig> = (config?) =>
};
const ir = next.ir as IContainerInfo;
const scope = Scope.createRootScope();
if (ir.state) {
const state = ir.state;
const fields = Object.keys(state).map<string>((stateName) => {
const value = generateCompositeType(state[stateName]);
const value = generateCompositeType(state[stateName], scope);
return `${stateName}: ${value},`;
});

View File

@ -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<unknown> = () => {
};
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<unknown> = () => {
const extConfigs = Object.keys(rest).map((extConfigName) => {
const value = (rest as Record<string, CompositeValue>)[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,
},

View File

@ -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<unknown> = () => {
const generator = createNodeGenerator({
plugins: [generateGlobalProps, generateCtrlLine],
nodePlugins: [generateGlobalProps, generateCtrlLine],
});
const plugin: BuilderComponentPlugin = async (pre: ICodeStruct) => {
@ -62,8 +63,9 @@ const pluginFactory: BuilderComponentPluginFactory<unknown> = () => {
};
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,

View File

@ -8,6 +8,7 @@ import {
ICodeStruct,
IProjectInfo,
} from '../../types';
import Scope from '../../utils/Scope';
const pluginFactory: BuilderComponentPluginFactory<unknown> = () => {
const plugin: BuilderComponentPlugin = async (pre: ICodeStruct) => {
@ -16,7 +17,8 @@ const pluginFactory: BuilderComponentPluginFactory<unknown> = () => {
};
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,

View File

@ -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<I, T, C> = (input: I, scope: IScope, config?: C, next?: BaseGenerator<I, T, C>) => T;
type CompositeTypeGenerator<I, T> =
| BaseGenerator<I, T, CompositeValueGeneratorOptions>
| Array<BaseGenerator<I, T, CompositeValueGeneratorOptions>>;
export interface CodePiece {
value: string;
type: PIECE_TYPE;
}
export type NodeGenerator<T> = (nodeItem: NodeDataType, scope: IScope) => T;
// FIXME: 在新的实现中,添加了第一参数 this: CustomHandlerSet 作为上下文。究其本质
// scopeBindings?: IScopeBindings;
// 这个组合只用来用来处理 CompositeValue 类型,不是这个类型的不要放在这里
export interface HandlerSet<T> {
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<string, T>;
boolean?: CompositeTypeGenerator<boolean, T>;
number?: CompositeTypeGenerator<number, T>;
expression?: CompositeTypeGenerator<JSExpression, T>;
function?: CompositeTypeGenerator<JSFunction, T>;
slot?: CompositeTypeGenerator<JSSlot, T>;
array?: CompositeTypeGenerator<JSONArray | CompositeArray, T>;
object?: CompositeTypeGenerator<JSONObject | CompositeObject, T>;
}
export type ExtGeneratorPlugin = (ctx: INodeGeneratorContext, nodeItem: NodeSchema) => CodePiece[];
export type NodeGeneratorConfig = {
handlers?: HandlerSet<string>;
plugins?: ExtGeneratorPlugin[];
};
export type NodeGenerator = (nodeItem: NodeDataType) => string;
export interface INodeGeneratorContext {
handlers: HandlerSet<string>;
plugins: ExtGeneratorPlugin[];
generator: NodeGenerator;
}
export type CompositeValueCustomHandler = (data: unknown) => string;
export type CompositeValueGeneratorOptions = {
handlers?: HandlerSet<string>;
containerHandler?: (value: string, isString: boolean, valStr: string) => string;
nodeGenerator?: NodeGenerator;
nodeGenerator?: NodeGenerator<string>;
};
export interface IScope {
// 作用域内定义
bindings?: IScopeBindings;
// TODO: 需要有上下文信息吗? 描述什么内容
createSubScope: (ownIndentifiers: string[]) => IScope;
}

View File

@ -3,3 +3,4 @@ export * from './deps';
export * from './error';
export * from './intermediate';
export * from './publisher';
export * from './jsx';

View File

@ -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<AttrData, CodePiece[], NodeGeneratorConfig>;
export type NodePlugin = BaseGenerator<NodeSchema, CodePiece[], NodeGeneratorConfig>;
export type NodeGeneratorConfig = {
handlers?: HandlerSet<string>;
tagMapping?: (input: string) => string;
attrPlugins?: AttrPlugin[];
nodePlugins?: NodePlugin[];
self?: NodeGenerator<string>;
};

View File

@ -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;

View File

@ -0,0 +1,27 @@
import { BaseGenerator, IScope } from '../types/core';
export function executeFunctionStack<I, T, C>(
input: I,
scope: IScope,
funcs: BaseGenerator<I, T, C> | Array<BaseGenerator<I, T, C>>,
baseFunc: BaseGenerator<I, T, C>,
config?: C,
): T {
const funcList: Array<BaseGenerator<I, T, C>> = [];
if (Array.isArray(funcs)) {
funcList.push(...funcs);
} else {
funcList.push(funcs);
}
let next: BaseGenerator<I, T, C> = 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);
}

View File

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

View File

@ -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>): 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');

View File

@ -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<T>(
children: unknown,
handlers: HandlerSet<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), []);
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<AttrData, CodePiece[], NodeGeneratorConfig>(
{ 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 场景(<Xxx {...(something)}/>)
@ -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<NodeSchema, CodePiece[], NodeGeneratorConfig>(
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<string> {
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<string> {
const newCfg = mergeNodeGeneratorConfig(defaultReactGeneratorConfig, cfg);
return createNodeGenerator(newCfg);
}

View File

@ -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<T>(
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;
}