From 779ea7c718194ff4c91cd48499cec30dbca6e5e6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=89=A7=E6=AF=85?= Date: Thu, 13 Aug 2020 17:31:11 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=F0=9F=8E=B8=20=E8=A7=A3=E5=86=B3?= =?UTF-8?q?=E9=80=9A=E8=BF=87=20Rax=20=E5=87=BA=E7=A0=81=E5=88=B0=E5=B0=8F?= =?UTF-8?q?=E7=A8=8B=E5=BA=8F=E7=9A=84=E6=97=B6=E5=80=99=E5=BE=AA=E7=8E=AF?= =?UTF-8?q?=E9=87=8C=E9=9D=A2=E6=B2=A1=E6=B3=95=E7=94=A8=E5=BE=AA=E7=8E=AF?= =?UTF-8?q?=E5=8F=98=E9=87=8F=E7=9A=84=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../component/rax/containerInjectContext.ts | 2 +- .../src/plugins/component/rax/jsx.ts | 212 +++++++++++++--- .../framework/rax/plugins/packageJSON.ts | 1 + .../code-generator/src/solutions/rax-app.ts | 4 +- .../code-generator/src/utils/OrderedSet.ts | 33 +++ .../code-generator/src/utils/ScopeBindings.ts | 52 ++++ .../code-generator/src/utils/compositeType.ts | 29 ++- .../src/utils/expressionParser.ts | 239 ++++++++++++++++++ .../code-generator/src/utils/nodeToJSX.ts | 39 ++- .../demo1/expected/demo-project/package.json | 1 + .../demo-project/src/pages/Home/index.jsx | 4 +- .../demo2/expected/demo-project/package.json | 1 + .../demo-project/src/pages/Home/index.jsx | 45 +++- .../test-cases/rax-app/demo2/schema.json5 | 37 ++- ...parseExpressionConvertThis2Context.test.ts | 79 ++++++ .../parseExpressionGetGlobalVariables.test.ts | 114 +++++++++ 16 files changed, 812 insertions(+), 80 deletions(-) create mode 100644 packages/code-generator/src/utils/OrderedSet.ts create mode 100644 packages/code-generator/src/utils/ScopeBindings.ts create mode 100644 packages/code-generator/src/utils/expressionParser.ts create mode 100644 packages/code-generator/test/utils/expressionParser/parseExpressionConvertThis2Context.test.ts create mode 100644 packages/code-generator/test/utils/expressionParser/parseExpressionGetGlobalVariables.test.ts diff --git a/packages/code-generator/src/plugins/component/rax/containerInjectContext.ts b/packages/code-generator/src/plugins/component/rax/containerInjectContext.ts index 659ccf933..228d7dad5 100644 --- a/packages/code-generator/src/plugins/component/rax/containerInjectContext.ts +++ b/packages/code-generator/src/plugins/component/rax/containerInjectContext.ts @@ -53,7 +53,7 @@ const pluginFactory: BuilderComponentPluginFactory = (config?) => return self._dataSourceEngine.dataSourceMap || {}; }, async reloadDataSource() { - self._dataSourceEngine.reloadDataSource(); + await self._dataSourceEngine.reloadDataSource(); }, get utils() { return self._utils; diff --git a/packages/code-generator/src/plugins/component/rax/jsx.ts b/packages/code-generator/src/plugins/component/rax/jsx.ts index 9b73f3ef1..eb6c513af 100644 --- a/packages/code-generator/src/plugins/component/rax/jsx.ts +++ b/packages/code-generator/src/plugins/component/rax/jsx.ts @@ -1,23 +1,31 @@ +import _ from 'lodash'; +import changeCase from 'change-case'; import { BuilderComponentPlugin, BuilderComponentPluginFactory, ChunkType, + CodePiece, + CompositeValue, FileType, + HandlerSet, ICodeChunk, ICodeStruct, IContainerInfo, - isJSExpression, - isJSFunction, JSExpression, - JSFunction, + NodeSchema, NpmInfo, + PIECE_TYPE, } from '../../../types'; import { RAX_CHUNK_NAME } from './const'; import { COMMON_CHUNK_NAME } from '../../../const/generator'; -import { createNodeGenerator, generateReactCtrlLine, generateString } from '../../../utils/nodeToJSX'; +import { createNodeGenerator, generateReactCtrlLine, generateString, generateAttr } from '../../../utils/nodeToJSX'; import { generateExpression } from '../../../utils/jsExpression'; +import { CustomHandlerSet, generateUnknownType } from '../../../utils/compositeType'; + +import { IScopeBindings, ScopeBindings } from '../../../utils/ScopeBindings'; +import { parseExpressionConvertThis2Context, parseExpressionGetGlobalVariables } from '../../../utils/expressionParser'; type PluginConfig = { fileType: string; @@ -30,17 +38,22 @@ const pluginFactory: BuilderComponentPluginFactory = (config?) => ...config, }; - const transformers = { - transformThis2Context, - transformJsExpr: (expr: string) => - isLiteralAtomicExpr(expr) ? expr : `__$$eval(() => (${transformThis2Context(expr)}))`, - transformLoopExpr: (expr: string) => `__$$evalArray(() => (${transformThis2Context(expr)}))`, - }; + // 什么都不做的的话,会有 3 个问题: + // 1. 小程序出码的时候,循环变量没法拿到 + // 2. 小程序出码的时候,很容易出现 Uncaught TypeError: Cannot read property 'avatar' of undefined 这样的异常(如下图的 50 行) -- 因为若直接出码,Rax 构建到小程序的时候会立即计算所有在视图中用到的变量 + // 3. 通过 this.xxx 能拿到的东西太多了,而且自定义的 methods 可能会无意间破坏 Rax 框架或小程序框架在页面 this 上的东东 + // const transformers = { + // transformThis2Context: (expr: string) => expr, + // transformJsExpr: (expr: string) => expr, + // transformLoopExpr: (expr: string) => expr, + // }; - const handlers = { - expression: (input: JSExpression) => transformers.transformJsExpr(generateExpression(input)), - function: (input: JSFunction) => transformers.transformJsExpr(input.value || 'null'), - }; + // 不转换 this.xxx 到 __$$context.xxx 的话,依然会有上述的 1 和 3 的问题。 + // const transformers = { + // transformThis2Context: (expr: string) => expr, + // transformJsExpr: (expr: string) => (isLiteralAtomicExpr(expr) ? expr : `__$$eval(() => (${expr}))`), + // transformLoopExpr: (expr: string) => `__$$evalArray(() => (${expr}))`, + // }; const plugin: BuilderComponentPlugin = async (pre: ICodeStruct) => { const next: ICodeStruct = { @@ -64,24 +77,57 @@ const pluginFactory: BuilderComponentPluginFactory = (config?) => // 然后过滤掉所有的别名 chunks next.chunks = next.chunks.filter((chunk) => !isImportAliasDefineChunk(chunk)); + const customHandlers: CustomHandlerSet = { + expression(this: CustomHandlerSet, input: JSExpression) { + return transformJsExpr(generateExpression(input), this); + }, + function(input) { + return transformThis2Context(input.value || 'null', this); + }, + loopDataExpr(input) { + return typeof input === 'string' ? transformLoopExpr(input, this) : ''; + }, + tagName: mapComponentNameToAliasOrKeepIt, + nodeAttr: generateNodeAttrForRax, + }; + + const generatorHandlers: HandlerSet = { + string: generateString, + expression: (input) => [customHandlers.expression?.(input) || 'null'], + function: (input) => [customHandlers.function?.(input) || 'null'], + }; + // 创建代码生成器 - const generator = createNodeGenerator( - { - string: generateString, - expression: (input) => [handlers.expression(input)], - function: (input) => [handlers.function(input)], - }, - [generateReactCtrlLine], - { - expression: (input) => (isJSExpression(input) ? handlers.expression(input) : ''), - function: (input) => (isJSFunction(input) ? handlers.function(input) : ''), - loopDataExpr: (input) => (typeof input === 'string' ? transformers.transformLoopExpr(input) : ''), - tagName: mapComponentNameToAliasOrKeepIt, - }, - ); + const commonNodeGenerator = createNodeGenerator(generatorHandlers, [generateReactCtrlLine], customHandlers); + + 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); + }; + + generatorHandlers.node = (node) => [raxCodeGenerator(node)]; + customHandlers.node = raxCodeGenerator; // 生成 JSX 代码 - const jsxContent = generator(ir); + const jsxContent = raxCodeGenerator(ir); + + next.chunks.push({ + type: ChunkType.STRING, + fileType: cfg.fileType, + name: COMMON_CHUNK_NAME.ExternalDepsImport, + content: `import { isMiniApp as __$$isMiniApp } from 'universal-env';`, + linkAfter: [], + }); next.chunks.push({ type: ChunkType.STRING, @@ -130,6 +176,14 @@ const pluginFactory: BuilderComponentPluginFactory = (config?) => export default pluginFactory; +function transformLoopExpr(expr: string, handlers: CustomHandlerSet) { + return `__$$evalArray(() => (${transformThis2Context(expr, handlers)}))`; +} + +function transformJsExpr(expr: string, handlers: CustomHandlerSet) { + return isLiteralAtomicExpr(expr) ? expr : `__$$eval(() => (${transformThis2Context(expr, handlers)}))`; +} + function isImportAliasDefineChunk( chunk: ICodeChunk, ): chunk is ICodeChunk & { @@ -159,11 +213,97 @@ function isLiteralAtomicExpr(expr: string): boolean { * 将所有的 this.xxx 替换为 __$$context.xxx * @param expr */ -function transformThis2Context(expr: string): string { - // TODO: 应该根据语法分析来搞 - // TODO: 如何替换自定义名字的循环变量?(generateReactCtrlLine) - return expr - .replace(/\bthis\.item\./, () => 'item.') - .replace(/\bthis\.index\./, () => 'index.') - .replace(/\bthis\./, () => '__$$context.'); +function transformThis2Context(expr: string, customHandlers: CustomHandlerSet): string { + // return expr + // .replace(/\bthis\.item\./g, () => 'item.') + // .replace(/\bthis\.index\./g, () => 'index.') + // .replace(/\bthis\./g, () => '__$$context.'); + + return parseExpressionConvertThis2Context(expr, '__$$context', customHandlers.scopeBindings?.getAllBindings() || []); +} + +function generateNodeAttrForRax(this: CustomHandlerSet, attrName: string, attrValue: CompositeValue): CodePiece[] { + if (!/^on/.test(attrName)) { + return generateAttr(attrName, attrValue, { + ...this, + nodeAttr: undefined, + }); + } + + // 先出个码 + const valueExpr = generateUnknownType(attrValue, this); + + // 查询当前作用域下的变量 + const currentScopeVariables = this.scopeBindings?.getAllBindings() || []; + if (currentScopeVariables.length <= 0) { + return [ + { + type: PIECE_TYPE.ATTR, + value: `${attrName}={${valueExpr}}`, + }, + ]; + } + + // 提取出所有的未定义的全局变量 + const undeclaredVariablesInValueExpr = parseExpressionGetGlobalVariables(valueExpr); + const referencedLocalVariables = _.intersection(undeclaredVariablesInValueExpr, currentScopeVariables); + if (referencedLocalVariables.length <= 0) { + return [ + { + type: PIECE_TYPE.ATTR, + value: `${attrName}={${valueExpr}}`, + }, + ]; + } + + const wrappedAttrValueExpr = [ + `(...__$$args) => {`, + ` if (__$$isMiniApp) {`, + ` const __$$event = __$$args[0];`, + ...referencedLocalVariables.map((localVar) => `const ${localVar} = __$$event.target.dataset.${localVar};`), + ` return (${valueExpr}).apply(this, __$$args);`, + ` } else {`, + ` return (${valueExpr}).apply(this, __$$args);`, + ` }`, + `}`, + ].join('\n'); + + return [ + ...referencedLocalVariables.map((localVar) => ({ + type: PIECE_TYPE.ATTR, + value: `data-${changeCase.snake(localVar)}={${localVar}}`, + })), + { + type: PIECE_TYPE.ATTR, + value: `${attrName}={${wrappedAttrValueExpr}}`, + }, + ]; +} + +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/project/framework/rax/plugins/packageJSON.ts b/packages/code-generator/src/plugins/project/framework/rax/plugins/packageJSON.ts index 86cdace2d..4da909235 100644 --- a/packages/code-generator/src/plugins/project/framework/rax/plugins/packageJSON.ts +++ b/packages/code-generator/src/plugins/project/framework/rax/plugins/packageJSON.ts @@ -36,6 +36,7 @@ const pluginFactory: BuilderComponentPluginFactory = () => { }, dependencies: { '@ali/lowcode-datasource-engine': '^0.1.0', + 'universal-env': '^3.2.0', rax: '^1.1.0', 'rax-app': '^2.0.0', 'rax-document': '^0.1.0', diff --git a/packages/code-generator/src/solutions/rax-app.ts b/packages/code-generator/src/solutions/rax-app.ts index 230c93813..85a0367f6 100644 --- a/packages/code-generator/src/solutions/rax-app.ts +++ b/packages/code-generator/src/solutions/rax-app.ts @@ -60,8 +60,6 @@ export default function createIceJsProjectBuilder(): IProjectBuilder { htmlEntry: [raxApp.plugins.entryDocument()], packageJSON: [raxApp.plugins.packageJSON()], }, - postProcessors: [ - // prettier() // 暂且禁用 prettier - ], + postProcessors: process.env.NODE_ENV !== 'test' ? [prettier()] : [], }); } diff --git a/packages/code-generator/src/utils/OrderedSet.ts b/packages/code-generator/src/utils/OrderedSet.ts new file mode 100644 index 000000000..3c58ddc73 --- /dev/null +++ b/packages/code-generator/src/utils/OrderedSet.ts @@ -0,0 +1,33 @@ +export class OrderedSet { + private _set = new Set(); + private _arr: T[] = []; + + constructor(items?: T[]) { + if (items) { + this._set = new Set(items); + this._arr = items.slice(0); + } + } + + add(item: T) { + if (!this._set.has(item)) { + this._set.add(item); + this._arr.push(item); + } + } + + delete(item: T) { + if (this._set.has(item)) { + this._set.delete(item); + this._arr.splice(this._arr.indexOf(item), 1); + } + } + + has(item: T) { + return this._set.has(item); + } + + toArray() { + return this._arr.slice(0); + } +} diff --git a/packages/code-generator/src/utils/ScopeBindings.ts b/packages/code-generator/src/utils/ScopeBindings.ts new file mode 100644 index 000000000..1f6032025 --- /dev/null +++ b/packages/code-generator/src/utils/ScopeBindings.ts @@ -0,0 +1,52 @@ +import { OrderedSet } from './OrderedSet'; + +export interface IScopeBindings { + readonly parent: IScopeBindings | null; + + hasBinding(varName: string): boolean; + hasOwnBinding(varName: string): boolean; + + addBinding(varName: string): void; + removeBinding(varName: string): void; + + getAllBindings(): string[]; + getAllOwnedBindings(): string[]; +} + +export class ScopeBindings implements IScopeBindings { + private _bindings = new OrderedSet(); + + constructor(public readonly parent: IScopeBindings | null = null) {} + + hasBinding(varName: string): boolean { + return this._bindings.has(varName) || !!this.parent?.hasBinding(varName); + } + + hasOwnBinding(varName: string): boolean { + return this._bindings.has(varName); + } + + addBinding(varName: string): void { + this._bindings.add(varName); + } + + removeBinding(varName: string): void { + this._bindings.delete(varName); + } + + getAllBindings(): string[] { + const allBindings = new OrderedSet(this._bindings.toArray()); + + for (let parent = this.parent; parent; parent = parent?.parent) { + parent.getAllOwnedBindings().forEach((varName) => { + allBindings.add(varName); + }); + } + + return allBindings.toArray(); + } + + getAllOwnedBindings(): string[] { + return this._bindings.toArray(); + } +} diff --git a/packages/code-generator/src/utils/compositeType.ts b/packages/code-generator/src/utils/compositeType.ts index 6c4e4875a..0993f11c5 100644 --- a/packages/code-generator/src/utils/compositeType.ts +++ b/packages/code-generator/src/utils/compositeType.ts @@ -12,22 +12,27 @@ import { JSSlot, NodeSchema, NodeData, + CodePiece, } from '../types'; import { generateExpression, generateFunction } from './jsExpression'; +import { IScopeBindings } from './ScopeBindings'; export interface CustomHandlerSet { - boolean?: (bool: boolean) => string; - number?: (num: number) => string; - string?: (str: string) => string; - array?: (arr: JSONArray | CompositeArray) => string; - object?: (obj: JSONObject | CompositeObject) => string; - expression?: (jsExpr: JSExpression) => string; - function?: (jsFunc: JSFunction) => string; - slot?: (jsSlot: JSSlot) => string; - node?: (node: NodeSchema) => string; - loopDataExpr?: (loopDataExpr: string) => string; - conditionExpr?: (conditionExpr: string) => string; - tagName?: (tagName: string) => string; + boolean?(this: CustomHandlerSet, bool: boolean): string; + number?(this: CustomHandlerSet, num: number): string; + string?(this: CustomHandlerSet, str: string): string; + array?(this: CustomHandlerSet, arr: JSONArray | CompositeArray): string; + object?(this: CustomHandlerSet, obj: JSONObject | CompositeObject): string; + expression?(this: CustomHandlerSet, jsExpr: JSExpression): string; + function?(this: CustomHandlerSet, jsFunc: JSFunction): string; + slot?(this: CustomHandlerSet, jsSlot: JSSlot): string; + node?(this: CustomHandlerSet, node: NodeSchema): string; + nodeAttrs?(this: CustomHandlerSet, node: NodeSchema): CodePiece[]; + nodeAttr?(this: CustomHandlerSet, attrName: string, attrValue: CompositeValue): CodePiece[]; + loopDataExpr?(this: CustomHandlerSet, loopDataExpr: string): string; + conditionExpr?(this: CustomHandlerSet, conditionExpr: string): string; + tagName?(this: CustomHandlerSet, tagName: string): string; + scopeBindings?: IScopeBindings; } function generateArray(value: CompositeArray, handlers: CustomHandlerSet): string { diff --git a/packages/code-generator/src/utils/expressionParser.ts b/packages/code-generator/src/utils/expressionParser.ts new file mode 100644 index 000000000..0b7755171 --- /dev/null +++ b/packages/code-generator/src/utils/expressionParser.ts @@ -0,0 +1,239 @@ +import * as parser from '@babel/parser'; +import generate from '@babel/generator'; +import traverse, { NodePath } from '@babel/traverse'; +import * as t from '@babel/types'; +import { isIdentifier, Node } from '@babel/types'; + +import { OrderedSet } from './OrderedSet'; + +export class ParseError extends Error { + constructor(public readonly expr: string, public readonly detail: unknown) { + super(`Failed to parse expression "${expr}"`); + Object.setPrototypeOf(this, new.target.prototype); + } +} + +const MAYBE_EXPRESSIONS: { + [k in Node['type']]?: { + // fields: Array + fields: string[] | ((node: Node) => string[]); + }; +} = { + ArrayExpression: { fields: ['elements'] }, + AssignmentExpression: { fields: ['left', 'right'] }, + BinaryExpression: { fields: ['left', 'right'] }, + CallExpression: { fields: ['arguments', 'callee'] }, + ConditionalExpression: { fields: ['test', 'consequent', 'alternate'] }, + DoWhileStatement: { fields: ['test'] }, + ExpressionStatement: { fields: ['expression'] }, + ForInStatement: { fields: ['right'] }, + ForStatement: { fields: ['init', 'test', 'update'] }, + IfStatement: { fields: ['test'] }, + LogicalExpression: { fields: ['left', 'right'] }, + MemberExpression: { + fields: (node) => (node.type === 'MemberExpression' && node.computed ? ['object', 'property'] : ['object']), + }, + NewExpression: { fields: ['callee', 'arguments'] }, + ObjectMethod: { + fields: (node) => (node.type === 'ObjectMethod' && node.computed ? ['key'] : []), + }, + ObjectProperty: { + fields: (node) => (node.type === 'ObjectProperty' && node.computed ? ['key', 'value'] : ['value']), + }, + ReturnStatement: { fields: ['argument'] }, + SequenceExpression: { fields: ['expressions'] }, + ParenthesizedExpression: { fields: ['expression'] }, + SwitchCase: { fields: ['test'] }, + SwitchStatement: { fields: ['discriminant'] }, + ThrowStatement: { fields: ['argument'] }, + UnaryExpression: { fields: ['argument'] }, + UpdateExpression: { fields: ['argument'] }, + VariableDeclarator: { fields: ['init'] }, + WhileStatement: { fields: ['test'] }, + WithStatement: { fields: ['object'] }, + AssignmentPattern: { fields: ['right'] }, + ArrowFunctionExpression: { fields: ['body'] }, + ClassExpression: { fields: ['superClass'] }, + ClassDeclaration: { fields: ['superClass'] }, + ExportDefaultDeclaration: { fields: ['declaration'] }, + ForOfStatement: { fields: ['right'] }, + ClassMethod: { fields: (node) => (node.type === 'ClassMethod' && node.computed ? ['key'] : []) }, + SpreadElement: { fields: ['argument'] }, + TaggedTemplateExpression: { fields: ['tag'] }, + TemplateLiteral: { fields: ['expressions'] }, + YieldExpression: { fields: ['argument'] }, + AwaitExpression: { fields: ['argument'] }, + OptionalMemberExpression: { + fields: (node) => (node.type === 'OptionalMemberExpression' && node.computed ? ['object', 'property'] : ['object']), + }, + OptionalCallExpression: { fields: ['callee', 'arguments'] }, + JSXSpreadAttribute: { fields: ['argument'] }, + BindExpression: { fields: ['object', 'callee'] }, + ClassProperty: { fields: (node) => (node.type === 'ClassProperty' && node.computed ? ['key', 'value'] : ['value']) }, + PipelineTopicExpression: { fields: ['expression'] }, + PipelineBareFunction: { fields: ['callee'] }, + ClassPrivateProperty: { fields: ['value'] }, + Decorator: { fields: ['expression'] }, + TupleExpression: { fields: ['elements'] }, + TSDeclareMethod: { fields: (node) => (node.type === 'TSDeclareMethod' && node.computed ? ['key'] : []) }, + TSPropertySignature: { + fields: (node) => (node.type === 'TSPropertySignature' && node.computed ? ['key', 'initializer'] : ['initializer']), + }, + + TSMethodSignature: { + fields: (node) => (node.type === 'TSMethodSignature' && node.computed ? ['key'] : []), + }, + TSAsExpression: { fields: ['expression'] }, + TSTypeAssertion: { fields: ['expression'] }, + TSEnumDeclaration: { fields: ['initializer'] }, + TSEnumMember: { fields: ['initializer'] }, + TSNonNullExpression: { fields: ['expression'] }, + TSExportAssignment: { fields: ['expression'] }, +}; + +export type ParseExpressionGetGlobalVariablesOptions = { filter?: (varName: string) => boolean }; + +const CROSS_THIS_SCOPE_TYPE_NODE: { + [k in Node['type']]?: boolean; +} = { + ArrowFunctionExpression: false, // 箭头函数不跨越 this 的 scope + FunctionExpression: true, + FunctionDeclaration: true, + // FunctionTypeAnnotation: false, // 这是 TS 定义 + // FunctionTypeParam: false, // 这是 TS 定义 + ClassDeclaration: true, + ClassExpression: true, + ClassBody: true, + ClassImplements: true, + ClassMethod: true, + ClassPrivateMethod: true, + ClassProperty: true, + ClassPrivateProperty: true, + DeclareClass: true, +}; + +export function parseExpressionGetGlobalVariables( + expr: string | null | undefined, + { filter = (x) => true }: ParseExpressionGetGlobalVariablesOptions = {}, +): string[] { + if (!expr) { + return []; + } + + try { + const undeclaredVars = new OrderedSet(); + + const ast = parser.parse(`!(${expr});`); + + const addUndeclaredIdentifierIfNeeded = (x: object | null | undefined, path: NodePath) => { + if (isIdentifier(x) && !path.scope.hasBinding(x.name)) { + undeclaredVars.add(x.name); + } + }; + + traverse(ast, { + enter(path) { + const node = path.node; + const expressionFields = MAYBE_EXPRESSIONS[node.type]?.fields; + if (expressionFields) { + (typeof expressionFields === 'function' ? expressionFields(node) : expressionFields).forEach((fieldName) => { + const fieldValue = node[fieldName as keyof typeof node]; + if (typeof fieldValue === 'object') { + if (Array.isArray(fieldValue)) { + fieldValue.forEach((item) => { + addUndeclaredIdentifierIfNeeded(item, path); + }); + } else { + addUndeclaredIdentifierIfNeeded(fieldValue, path); + } + } + }); + } + }, + }); + + return undeclaredVars.toArray().filter(filter); + } catch (e) { + throw new ParseError(expr, e); + } +} + +export function parseExpressionConvertThis2Context( + expr: string, + contextName: string = '__$$context', + localVariables: string[] = [], +): string { + if (!expr) { + return expr; + } + + try { + const exprAst = parser.parseExpression(expr); + const exprWrapAst = t.expressionStatement(exprAst); + const fileAst = t.file(t.program([exprWrapAst])); + + const localVariablesSet = new Set(localVariables); + + let thisScopeLevel = CROSS_THIS_SCOPE_TYPE_NODE[exprAst.type] ? -1 : 0; + traverse(fileAst, { + enter(path) { + if (CROSS_THIS_SCOPE_TYPE_NODE[path.node.type]) { + thisScopeLevel++; + } + }, + exit(path) { + if (CROSS_THIS_SCOPE_TYPE_NODE[path.node.type]) { + thisScopeLevel--; + } + }, + MemberExpression(path) { + if (!path.isMemberExpression()) { + return; + } + + const obj = path.get('object'); + if (!obj.isThisExpression()) { + return; + } + + // 处理局部变量 + if (!path.node.computed) { + const prop = path.get('property'); + if (prop.isIdentifier() && localVariablesSet.has(prop.node.name)) { + path.replaceWith(t.identifier(prop.node.name)); + return; + } + } + + // 替换 this (只在顶层替换) + if (thisScopeLevel <= 0) { + obj.replaceWith(t.identifier(contextName)); + } + }, + ThisExpression(path) { + if (!path.isThisExpression()) { + return; + } + + // MemberExpression 中的 this.xxx 已经处理过了 + if (path.parent.type === 'MemberExpression') { + return; + } + + if (thisScopeLevel <= 0) { + path.replaceWith(t.identifier(contextName)); + } + }, + }); + + const { code } = generate(exprWrapAst.expression, { sourceMaps: false }); + return code; + } catch (e) { + // throw new ParseError(expr, e); + throw e; + } +} + +function indent(level: number) { + return ' '.repeat(level); +} diff --git a/packages/code-generator/src/utils/nodeToJSX.ts b/packages/code-generator/src/utils/nodeToJSX.ts index a6fe8614a..7b5b96c6f 100644 --- a/packages/code-generator/src/utils/nodeToJSX.ts +++ b/packages/code-generator/src/utils/nodeToJSX.ts @@ -35,11 +35,21 @@ export function handleChildren(children: NodeData | NodeData[], handlers: Handle } } -export function generateAttr(attrName: string, attrValue: CompositeValue, handlers: CustomHandlerSet): CodePiece[] { +export function generateAttr( + attrName: string, + attrValue: CompositeValue, + customHandlers: CustomHandlerSet, +): CodePiece[] { + if (customHandlers.nodeAttr) { + return customHandlers.nodeAttr(attrName, attrValue); + } + + // TODO: 这两个为啥要特殊处理?? if (attrName === 'initValue' || attrName === 'labelCol') { return []; } - const [isString, valueStr] = generateCompositeType(attrValue, handlers); + + const [isString, valueStr] = generateCompositeType(attrValue, customHandlers); return [ { value: `${attrName}=${isString ? `"${valueStr}"` : `{${valueStr}}`}`, @@ -48,7 +58,11 @@ export function generateAttr(attrName: string, attrValue: CompositeValue, handle ]; } -export function generateAttrs(nodeItem: NodeSchema, handlers: CustomHandlerSet): CodePiece[] { +export function generateAttrs(nodeItem: NodeSchema, customHandlers: CustomHandlerSet): CodePiece[] { + if (customHandlers.nodeAttrs) { + return customHandlers.nodeAttrs(nodeItem); + } + const { props } = nodeItem; let pieces: CodePiece[] = []; @@ -56,12 +70,12 @@ export function generateAttrs(nodeItem: NodeSchema, handlers: CustomHandlerSet): if (props) { if (!Array.isArray(props)) { Object.keys(props).forEach((propName: string) => { - pieces = pieces.concat(generateAttr(propName, props[propName], handlers)); + pieces = pieces.concat(generateAttr(propName, props[propName], customHandlers)); }); } else { props.forEach((prop) => { if (prop.name && !prop.spread) { - pieces = pieces.concat(generateAttr(prop.name, prop.value, handlers)); + pieces = pieces.concat(generateAttr(prop.name, prop.value, customHandlers)); } // TODO: 处理 spread 场景() @@ -97,10 +111,11 @@ export function generateReactCtrlLine(nodeItem: NodeSchema, handlers: CustomHand const loopItemName = nodeItem.loopArgs?.[0] || 'item'; const loopIndexName = nodeItem.loopArgs?.[1] || 'index'; - // TODO: 静态的值可以抽离出来? - const loopDataExpr = (handlers.loopDataExpr || _.identity)( - isJSExpression(nodeItem.loop) ? `(${nodeItem.loop.value})` : `(${JSON.stringify(nodeItem.loop)})`, - ); + const rawLoopDataExpr = isJSExpression(nodeItem.loop) + ? `(${nodeItem.loop.value})` + : `(${JSON.stringify(nodeItem.loop)})`; + + const loopDataExpr = handlers.loopDataExpr ? handlers.loopDataExpr(rawLoopDataExpr) : rawLoopDataExpr; pieces.unshift({ value: `${loopDataExpr}.map((${loopItemName}, ${loopIndexName}) => (`, @@ -115,7 +130,8 @@ export function generateReactCtrlLine(nodeItem: NodeSchema, handlers: CustomHand if (nodeItem.condition) { const [isString, value] = generateCompositeType(nodeItem.condition, handlers); - const conditionExpr = (handlers.conditionExpr || _.identity)(isString ? `'${value}'` : value); + const rawConditionExpr = isString ? `'${value}'` : value; + const conditionExpr = handlers.conditionExpr ? handlers.conditionExpr(rawConditionExpr) : rawConditionExpr; pieces.unshift({ value: `(${conditionExpr}) && (`, @@ -149,7 +165,8 @@ export function linkPieces(pieces: CodePiece[], handlers: CustomHandlerSet): str throw new CodeGeneratorError('One node only need one tag define'); } - const tagName = (handlers.tagName || _.identity)(tagsPieces[0].value); + const rawTagName = tagsPieces[0].value; + const tagName = handlers.tagName ? handlers.tagName(rawTagName) : rawTagName; const beforeParts = pieces .filter((p) => p.type === PIECE_TYPE.BEFORE) diff --git a/packages/code-generator/test-cases/rax-app/demo1/expected/demo-project/package.json b/packages/code-generator/test-cases/rax-app/demo1/expected/demo-project/package.json index f2ad5aa15..6d3ae68a5 100644 --- a/packages/code-generator/test-cases/rax-app/demo1/expected/demo-project/package.json +++ b/packages/code-generator/test-cases/rax-app/demo1/expected/demo-project/package.json @@ -10,6 +10,7 @@ }, "dependencies": { "@ali/lowcode-datasource-engine": "^0.1.0", + "universal-env": "^3.2.0", "rax": "^1.1.0", "rax-app": "^2.0.0", "rax-document": "^0.1.0", diff --git a/packages/code-generator/test-cases/rax-app/demo1/expected/demo-project/src/pages/Home/index.jsx b/packages/code-generator/test-cases/rax-app/demo1/expected/demo-project/src/pages/Home/index.jsx index 89960d6ef..bb5687ef1 100644 --- a/packages/code-generator/test-cases/rax-app/demo1/expected/demo-project/src/pages/Home/index.jsx +++ b/packages/code-generator/test-cases/rax-app/demo1/expected/demo-project/src/pages/Home/index.jsx @@ -8,6 +8,8 @@ import Text from 'rax-text'; import { create as __$$createDataSourceEngine } from '@ali/lowcode-datasource-engine'; +import { isMiniApp as __$$isMiniApp } from 'universal-env'; + import __$$projectUtils from '../../utils'; import './index.css'; @@ -50,7 +52,7 @@ class Home$$Page extends Component { return self._dataSourceEngine.dataSourceMap || {}; }, async reloadDataSource() { - self._dataSourceEngine.reloadDataSource(); + await self._dataSourceEngine.reloadDataSource(); }, get utils() { return self._utils; diff --git a/packages/code-generator/test-cases/rax-app/demo2/expected/demo-project/package.json b/packages/code-generator/test-cases/rax-app/demo2/expected/demo-project/package.json index f14719b11..893e55365 100644 --- a/packages/code-generator/test-cases/rax-app/demo2/expected/demo-project/package.json +++ b/packages/code-generator/test-cases/rax-app/demo2/expected/demo-project/package.json @@ -10,6 +10,7 @@ }, "dependencies": { "@ali/lowcode-datasource-engine": "^0.1.0", + "universal-env": "^3.2.0", "rax": "^1.1.0", "rax-app": "^2.0.0", "rax-document": "^0.1.0", diff --git a/packages/code-generator/test-cases/rax-app/demo2/expected/demo-project/src/pages/Home/index.jsx b/packages/code-generator/test-cases/rax-app/demo2/expected/demo-project/src/pages/Home/index.jsx index 1cad2aa0c..42be52c51 100644 --- a/packages/code-generator/test-cases/rax-app/demo2/expected/demo-project/src/pages/Home/index.jsx +++ b/packages/code-generator/test-cases/rax-app/demo2/expected/demo-project/src/pages/Home/index.jsx @@ -10,12 +10,15 @@ import Image from 'rax-image'; import { create as __$$createDataSourceEngine } from '@ali/lowcode-datasource-engine'; +import { isMiniApp as __$$isMiniApp } from 'universal-env'; + import __$$projectUtils from '../../utils'; import './index.css'; class Home$$Page extends Component { state = { + clickCount: 0, user: { name: '张三', age: 18, avatar: 'https://gw.alicdn.com/tfs/TB1Ui9BMkY2gK0jSZFgXXc5OFXa-50-50.png' }, orders: [ { @@ -61,7 +64,7 @@ class Home$$Page extends Component { source={{ uri: __$$eval(() => __$$context.state.user.avatar) }} style={{ width: '32px', height: '32px' }} /> - __$$context.hello)}> + {__$$eval(() => __$$context.state.user.name)} {__$$eval(() => __$$context.state.user.age)}岁 @@ -70,29 +73,47 @@ class Home$$Page extends Component { === Orders: === - {__$$evalArray(() => __$$context.state.orders).map((item, index) => ( + {__$$evalArray(() => __$$context.state.orders).map((order, index) => ( - function () { - __$$context.utils.recordEvent(`CLICK_ORDER`, item.title); - }, - )} + data-order={order} + onClick={(...__$$args) => { + if (__$$isMiniApp) { + const __$$event = __$$args[0]; + const order = __$$event.target.dataset.order; + return function () { + __$$context.utils.recordEvent(`CLICK_ORDER`, order.title); + }.apply(this, __$$args); + } else { + return function () { + __$$context.utils.recordEvent(`CLICK_ORDER`, order.title); + }.apply(this, __$$args); + } + }} > - item.coverUrl) }} style={{ width: '80px', height: '60px' }} /> + order.coverUrl) }} style={{ width: '80px', height: '60px' }} /> - {__$$eval(() => item.title)} - {__$$eval(() => __$$context.utils.formatPrice(item.price, '元'))} + {__$$eval(() => order.title)} + {__$$eval(() => __$$context.utils.formatPrice(order.price, '元'))} ))} + + 点击次数:{__$$eval(() => __$$context.state.clickCount)}(点击加 1) + 操作提示: 1. 点击会员名,可以弹出 Toast "Hello xxx!" 2. 点击订单,会记录点击的订单信息,并弹出 Toast 提示 + 3. 最下面的【点击次数】,点一次应该加 1 ); @@ -112,7 +133,7 @@ class Home$$Page extends Component { return self._dataSourceEngine.dataSourceMap || {}; }, async reloadDataSource() { - self._dataSourceEngine.reloadDataSource(); + await self._dataSourceEngine.reloadDataSource(); }, get utils() { return self._utils; diff --git a/packages/code-generator/test-cases/rax-app/demo2/schema.json5 b/packages/code-generator/test-cases/rax-app/demo2/schema.json5 index d735dafde..dabdd8aa1 100644 --- a/packages/code-generator/test-cases/rax-app/demo2/schema.json5 +++ b/packages/code-generator/test-cases/rax-app/demo2/schema.json5 @@ -36,6 +36,7 @@ componentName: 'Page', fileName: 'home', state: { + clickCount: 0, user: { name: '张三', age: 18, avatar: 'https://gw.alicdn.com/tfs/TB1Ui9BMkY2gK0jSZFgXXc5OFXa-50-50.png' }, orders: [ { @@ -176,11 +177,12 @@ type: 'JSExpression', value: 'this.state.orders', }, + loopArgs: ['order', 'index'], props: { style: { flexDirection: 'row' }, onClick: { type: 'JSFunction', - value: 'function(){ this.utils.recordEvent(`CLICK_ORDER`, this.item.title) }', + value: 'function(){ this.utils.recordEvent(`CLICK_ORDER`, this.order.title) }', }, }, children: [ @@ -193,7 +195,7 @@ source: { uri: { type: 'JSExpression', - value: 'this.item.coverUrl', + value: 'this.order.coverUrl', }, }, style: { @@ -211,20 +213,42 @@ componentName: 'Text', children: { type: 'JSExpression', - value: 'this.item.title', + value: 'this.order.title', }, }, { componentName: 'Text', children: { type: 'JSExpression', - value: 'this.utils.formatPrice(this.item.price, "元")', + value: 'this.utils.formatPrice(this.order.price, "元")', }, }, ], }, ], }, + { + componentName: 'View', + props: { + onClick: { + type: 'JSFunction', + value: 'function (){ this.setState({ clickCount: this.state.clickCount + 1 }) }', + }, + }, + children: [ + { + componentName: 'Text', + children: [ + '点击次数:', + { + type: 'JSExpression', + value: 'this.state.clickCount', + }, + '(点击加 1)', + ], + }, + ], + }, { componentName: 'View', children: [ @@ -243,6 +267,11 @@ props: {}, children: '2. 点击订单,会记录点击的订单信息,并弹出 Toast 提示', }, + { + componentName: 'Text', + props: {}, + children: '3. 最下面的【点击次数】,点一次应该加 1', + }, ], }, ], diff --git a/packages/code-generator/test/utils/expressionParser/parseExpressionConvertThis2Context.test.ts b/packages/code-generator/test/utils/expressionParser/parseExpressionConvertThis2Context.test.ts new file mode 100644 index 000000000..90c30b3a0 --- /dev/null +++ b/packages/code-generator/test/utils/expressionParser/parseExpressionConvertThis2Context.test.ts @@ -0,0 +1,79 @@ +import test from 'ava'; +import type { ExecutionContext, Macro } from 'ava'; +import { parseExpressionConvertThis2Context } from '../../../src/utils/expressionParser'; + +const macro: Macro = ( + t: ExecutionContext<{}>, + input: [string, string, string[]], + expected: string, + error?: { message: RegExp }, +) => { + if (!error) { + t.deepEqual(parseExpressionConvertThis2Context(input[0], input[1], input[2]), expected); + } else { + t.throws(() => { + t.deepEqual(parseExpressionConvertThis2Context(input[0], input[1], input[2]), expected); + }, error.message); + } +}; + +macro.title = (providedTitle: string | undefined, ...args: any[]): string => { + const [input, expected] = args; + return providedTitle || `after convert this to context "${input[0]}" should be "${expected}"`.replace(/\n|\s+/g, ' '); +}; + +test(macro, ['this.hello', '__$$context', []], '__$$context.hello'); +test(macro, ['this.utils.recordEvent', '__$$context', []], '__$$context.utils.recordEvent'); + +test( + macro, + ['this.utils.recordEvent.bind(this)', '__$$context', []], + '__$$context.utils.recordEvent.bind(__$$context)', +); + +test(macro, ['this.item', '__$$context', ['item']], 'item'); + +test(macro, ['this.user.name', '__$$context', ['user']], 'user.name'); + +test(macro, ['function (){}', '__$$context', []], 'function () {}'); + +test( + macro, + ['function (){ this.utils.Toast.show("Hello world!") }', '__$$context'], + 'function () {\n __$$context.utils.Toast.show("Hello world!");\n}', +); + +// 变量能被替换掉 +test( + macro, + ['function (){ this.utils.recordEvent("click", this.item) }', '__$$context', ['item']], + 'function () {\n __$$context.utils.recordEvent("click", item);\n}', +); + +// 只替换顶层的,不替换内层 +test( + macro, + ['function (){ return function (){ this.utils.recordEvent("click", this.item) } }', '__$$context', ['item']], + 'function () {\n return function () {\n this.utils.recordEvent("click", item);\n };\n}', +); + +// 只替换顶层的,不替换内层 +test( + macro, + ['function onClick(){ return function (){ this.utils.recordEvent("click", this.item) } }', '__$$context', ['item']], + 'function onClick() {\n return function () {\n this.utils.recordEvent("click", item);\n };\n}', +); + +// 只替换顶层的,不替换内层 +test( + macro, + ['() => { return function (){ this.utils.recordEvent("click", this.item) } }', '__$$context', ['item']], + '() => {\n return function () {\n this.utils.recordEvent("click", item);\n };\n}', +); + +// 但是若内层有用箭头函数定义的则还是要替换下 +test( + macro, + ['() => { return () => { this.utils.recordEvent("click", this.item) } }', '__$$context', ['item']], + '() => {\n return () => {\n __$$context.utils.recordEvent("click", item);\n };\n}', +); diff --git a/packages/code-generator/test/utils/expressionParser/parseExpressionGetGlobalVariables.test.ts b/packages/code-generator/test/utils/expressionParser/parseExpressionGetGlobalVariables.test.ts new file mode 100644 index 000000000..d91566b9e --- /dev/null +++ b/packages/code-generator/test/utils/expressionParser/parseExpressionGetGlobalVariables.test.ts @@ -0,0 +1,114 @@ +import test from 'ava'; +import type { ExecutionContext, Macro } from 'ava'; +import { + parseExpressionGetGlobalVariables, + ParseExpressionGetGlobalVariablesOptions, +} from '../../../src/utils/expressionParser'; + +const macro: Macro = ( + t: ExecutionContext<{}>, + input: [string | null | undefined, ParseExpressionGetGlobalVariablesOptions], + expected: string[], + error?: { message: RegExp }, +) => { + if (!error) { + t.deepEqual(parseExpressionGetGlobalVariables(input[0], input[1]), expected); + } else { + t.throws(() => { + t.deepEqual(parseExpressionGetGlobalVariables(input[0], input[1]), expected); + }, error.message); + } +}; + +macro.title = (providedTitle: string | undefined, ...args: any[]): string => { + const [input, expected] = args; + return providedTitle || `global variables of "${input[0]}" should be "${expected.join(', ')}"`; +}; + +test(macro, ['function (){ }', {}], []); +test(macro, ['function (){ __$$context.utils.Toast.show("Hello world!") }', {}], ['__$$context']); + +test(macro, ['function (){ __$$context.utils.formatPrice(item.price1, "元") }', {}], ['__$$context', 'item']); + +test( + macro, + [ + 'function (){ __$$context.utils.formatPrice(item2, "元"); }', + { filter: (varName: string) => !/^__\$\$/.test(varName) }, + ], + ['item2'], +); + +test( + macro, + [ + 'function (){ __$$context.utils.log(item3, [item4, item5]); }', + { filter: (varName: string) => !/^__\$\$/.test(varName) }, + ], + ['item3', 'item4', 'item5'], +); + +test( + macro, + ['function (){ item3[item4]("Hello"); }', { filter: (varName: string) => !/^__\$\$/.test(varName) }], + ['item3', 'item4'], +); + +test(macro, ['function (){ item3("Hello"); }', { filter: (varName: string) => !/^__\$\$/.test(varName) }], ['item3']); + +test( + macro, + ['function foo(){ foo[item3]("Hello"); }', { filter: (varName: string) => !/^__\$\$/.test(varName) }], + ['item3'], +); + +// isAssignmentExpression/right +test( + macro, + ['function (){ let foo; foo = item3; foo(); }', { filter: (varName: string) => !/^__\$\$/.test(varName) }], + ['item3'], +); + +// isAssignmentExpression/left +test( + macro, + ['function (){ foo = item3; foo(); }', { filter: (varName: string) => !/^__\$\$/.test(varName) }], + ['foo', 'item3'], +); + +// isVariableDeclarator +test( + macro, + ['function (){ const foo = item3; foo(); }', { filter: (varName: string) => !/^__\$\$/.test(varName) }], + ['item3'], +); + +// isVariableDeclarator +test( + macro, + ['function (){ let foo = item3; foo(); }', { filter: (varName: string) => !/^__\$\$/.test(varName) }], + ['item3'], +); + +// isVariableDeclarator +test( + macro, + ['function (){ var foo = item3; foo(); }', { filter: (varName: string) => !/^__\$\$/.test(varName) }], + ['item3'], +); + +// isTemplateLiteral +test( + macro, + ['function (){ console.log(`Hello ${item3};`); }', { filter: (varName: string) => !/^__\$\$/.test(varName) }], + ['console', 'item3'], +); + +// isBinaryExpression +test( + macro, + ['function (){ console.log(item2 | item3); }', { filter: (varName: string) => !/^__\$\$/.test(varName) }], + ['console', 'item2', 'item3'], +); + +// TODO: 补充更多类型的测试用例