mirror of
https://github.com/alibaba/lowcode-engine.git
synced 2026-03-05 01:37:17 +00:00
feat: 🎸 解决通过 Rax 出码到小程序的时候循环里面没法用循环变量的问题
This commit is contained in:
parent
6cd07524b6
commit
779ea7c718
@ -53,7 +53,7 @@ const pluginFactory: BuilderComponentPluginFactory<PluginConfig> = (config?) =>
|
|||||||
return self._dataSourceEngine.dataSourceMap || {};
|
return self._dataSourceEngine.dataSourceMap || {};
|
||||||
},
|
},
|
||||||
async reloadDataSource() {
|
async reloadDataSource() {
|
||||||
self._dataSourceEngine.reloadDataSource();
|
await self._dataSourceEngine.reloadDataSource();
|
||||||
},
|
},
|
||||||
get utils() {
|
get utils() {
|
||||||
return self._utils;
|
return self._utils;
|
||||||
|
|||||||
@ -1,23 +1,31 @@
|
|||||||
|
import _ from 'lodash';
|
||||||
|
import changeCase from 'change-case';
|
||||||
import {
|
import {
|
||||||
BuilderComponentPlugin,
|
BuilderComponentPlugin,
|
||||||
BuilderComponentPluginFactory,
|
BuilderComponentPluginFactory,
|
||||||
ChunkType,
|
ChunkType,
|
||||||
|
CodePiece,
|
||||||
|
CompositeValue,
|
||||||
FileType,
|
FileType,
|
||||||
|
HandlerSet,
|
||||||
ICodeChunk,
|
ICodeChunk,
|
||||||
ICodeStruct,
|
ICodeStruct,
|
||||||
IContainerInfo,
|
IContainerInfo,
|
||||||
isJSExpression,
|
|
||||||
isJSFunction,
|
|
||||||
JSExpression,
|
JSExpression,
|
||||||
JSFunction,
|
NodeSchema,
|
||||||
NpmInfo,
|
NpmInfo,
|
||||||
|
PIECE_TYPE,
|
||||||
} from '../../../types';
|
} from '../../../types';
|
||||||
|
|
||||||
import { RAX_CHUNK_NAME } from './const';
|
import { RAX_CHUNK_NAME } from './const';
|
||||||
import { COMMON_CHUNK_NAME } from '../../../const/generator';
|
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 { generateExpression } from '../../../utils/jsExpression';
|
||||||
|
import { CustomHandlerSet, generateUnknownType } from '../../../utils/compositeType';
|
||||||
|
|
||||||
|
import { IScopeBindings, ScopeBindings } from '../../../utils/ScopeBindings';
|
||||||
|
import { parseExpressionConvertThis2Context, parseExpressionGetGlobalVariables } from '../../../utils/expressionParser';
|
||||||
|
|
||||||
type PluginConfig = {
|
type PluginConfig = {
|
||||||
fileType: string;
|
fileType: string;
|
||||||
@ -30,17 +38,22 @@ const pluginFactory: BuilderComponentPluginFactory<PluginConfig> = (config?) =>
|
|||||||
...config,
|
...config,
|
||||||
};
|
};
|
||||||
|
|
||||||
const transformers = {
|
// 什么都不做的的话,会有 3 个问题:
|
||||||
transformThis2Context,
|
// 1. 小程序出码的时候,循环变量没法拿到
|
||||||
transformJsExpr: (expr: string) =>
|
// 2. 小程序出码的时候,很容易出现 Uncaught TypeError: Cannot read property 'avatar' of undefined 这样的异常(如下图的 50 行) -- 因为若直接出码,Rax 构建到小程序的时候会立即计算所有在视图中用到的变量
|
||||||
isLiteralAtomicExpr(expr) ? expr : `__$$eval(() => (${transformThis2Context(expr)}))`,
|
// 3. 通过 this.xxx 能拿到的东西太多了,而且自定义的 methods 可能会无意间破坏 Rax 框架或小程序框架在页面 this 上的东东
|
||||||
transformLoopExpr: (expr: string) => `__$$evalArray(() => (${transformThis2Context(expr)}))`,
|
// const transformers = {
|
||||||
};
|
// transformThis2Context: (expr: string) => expr,
|
||||||
|
// transformJsExpr: (expr: string) => expr,
|
||||||
|
// transformLoopExpr: (expr: string) => expr,
|
||||||
|
// };
|
||||||
|
|
||||||
const handlers = {
|
// 不转换 this.xxx 到 __$$context.xxx 的话,依然会有上述的 1 和 3 的问题。
|
||||||
expression: (input: JSExpression) => transformers.transformJsExpr(generateExpression(input)),
|
// const transformers = {
|
||||||
function: (input: JSFunction) => transformers.transformJsExpr(input.value || 'null'),
|
// transformThis2Context: (expr: string) => expr,
|
||||||
};
|
// transformJsExpr: (expr: string) => (isLiteralAtomicExpr(expr) ? expr : `__$$eval(() => (${expr}))`),
|
||||||
|
// transformLoopExpr: (expr: string) => `__$$evalArray(() => (${expr}))`,
|
||||||
|
// };
|
||||||
|
|
||||||
const plugin: BuilderComponentPlugin = async (pre: ICodeStruct) => {
|
const plugin: BuilderComponentPlugin = async (pre: ICodeStruct) => {
|
||||||
const next: ICodeStruct = {
|
const next: ICodeStruct = {
|
||||||
@ -64,24 +77,57 @@ const pluginFactory: BuilderComponentPluginFactory<PluginConfig> = (config?) =>
|
|||||||
// 然后过滤掉所有的别名 chunks
|
// 然后过滤掉所有的别名 chunks
|
||||||
next.chunks = next.chunks.filter((chunk) => !isImportAliasDefineChunk(chunk));
|
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> = {
|
||||||
|
string: generateString,
|
||||||
|
expression: (input) => [customHandlers.expression?.(input) || 'null'],
|
||||||
|
function: (input) => [customHandlers.function?.(input) || 'null'],
|
||||||
|
};
|
||||||
|
|
||||||
// 创建代码生成器
|
// 创建代码生成器
|
||||||
const generator = createNodeGenerator(
|
const commonNodeGenerator = createNodeGenerator(generatorHandlers, [generateReactCtrlLine], customHandlers);
|
||||||
{
|
|
||||||
string: generateString,
|
const raxCodeGenerator = (node: NodeSchema): string => {
|
||||||
expression: (input) => [handlers.expression(input)],
|
if (node.loop) {
|
||||||
function: (input) => [handlers.function(input)],
|
const loopItemName = node.loopArgs?.[0] || 'item';
|
||||||
},
|
const loopIndexName = node.loopArgs?.[1] || 'index';
|
||||||
[generateReactCtrlLine],
|
|
||||||
{
|
return runInNewScope({
|
||||||
expression: (input) => (isJSExpression(input) ? handlers.expression(input) : ''),
|
scopeHost: customHandlers,
|
||||||
function: (input) => (isJSFunction(input) ? handlers.function(input) : ''),
|
newScopeOwnVariables: [loopItemName, loopIndexName],
|
||||||
loopDataExpr: (input) => (typeof input === 'string' ? transformers.transformLoopExpr(input) : ''),
|
run: () => commonNodeGenerator(node),
|
||||||
tagName: mapComponentNameToAliasOrKeepIt,
|
});
|
||||||
},
|
}
|
||||||
);
|
|
||||||
|
return commonNodeGenerator(node);
|
||||||
|
};
|
||||||
|
|
||||||
|
generatorHandlers.node = (node) => [raxCodeGenerator(node)];
|
||||||
|
customHandlers.node = raxCodeGenerator;
|
||||||
|
|
||||||
// 生成 JSX 代码
|
// 生成 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({
|
next.chunks.push({
|
||||||
type: ChunkType.STRING,
|
type: ChunkType.STRING,
|
||||||
@ -130,6 +176,14 @@ const pluginFactory: BuilderComponentPluginFactory<PluginConfig> = (config?) =>
|
|||||||
|
|
||||||
export default pluginFactory;
|
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(
|
function isImportAliasDefineChunk(
|
||||||
chunk: ICodeChunk,
|
chunk: ICodeChunk,
|
||||||
): chunk is ICodeChunk & {
|
): chunk is ICodeChunk & {
|
||||||
@ -159,11 +213,97 @@ function isLiteralAtomicExpr(expr: string): boolean {
|
|||||||
* 将所有的 this.xxx 替换为 __$$context.xxx
|
* 将所有的 this.xxx 替换为 __$$context.xxx
|
||||||
* @param expr
|
* @param expr
|
||||||
*/
|
*/
|
||||||
function transformThis2Context(expr: string): string {
|
function transformThis2Context(expr: string, customHandlers: CustomHandlerSet): string {
|
||||||
// TODO: 应该根据语法分析来搞
|
// return expr
|
||||||
// TODO: 如何替换自定义名字的循环变量?(generateReactCtrlLine)
|
// .replace(/\bthis\.item\./g, () => 'item.')
|
||||||
return expr
|
// .replace(/\bthis\.index\./g, () => 'index.')
|
||||||
.replace(/\bthis\.item\./, () => 'item.')
|
// .replace(/\bthis\./g, () => '__$$context.');
|
||||||
.replace(/\bthis\.index\./, () => 'index.')
|
|
||||||
.replace(/\bthis\./, () => '__$$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<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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -36,6 +36,7 @@ const pluginFactory: BuilderComponentPluginFactory<unknown> = () => {
|
|||||||
},
|
},
|
||||||
dependencies: {
|
dependencies: {
|
||||||
'@ali/lowcode-datasource-engine': '^0.1.0',
|
'@ali/lowcode-datasource-engine': '^0.1.0',
|
||||||
|
'universal-env': '^3.2.0',
|
||||||
rax: '^1.1.0',
|
rax: '^1.1.0',
|
||||||
'rax-app': '^2.0.0',
|
'rax-app': '^2.0.0',
|
||||||
'rax-document': '^0.1.0',
|
'rax-document': '^0.1.0',
|
||||||
|
|||||||
@ -60,8 +60,6 @@ export default function createIceJsProjectBuilder(): IProjectBuilder {
|
|||||||
htmlEntry: [raxApp.plugins.entryDocument()],
|
htmlEntry: [raxApp.plugins.entryDocument()],
|
||||||
packageJSON: [raxApp.plugins.packageJSON()],
|
packageJSON: [raxApp.plugins.packageJSON()],
|
||||||
},
|
},
|
||||||
postProcessors: [
|
postProcessors: process.env.NODE_ENV !== 'test' ? [prettier()] : [],
|
||||||
// prettier() // 暂且禁用 prettier
|
|
||||||
],
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
33
packages/code-generator/src/utils/OrderedSet.ts
Normal file
33
packages/code-generator/src/utils/OrderedSet.ts
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
export class OrderedSet<T> {
|
||||||
|
private _set = new Set<T>();
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
52
packages/code-generator/src/utils/ScopeBindings.ts
Normal file
52
packages/code-generator/src/utils/ScopeBindings.ts
Normal file
@ -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<string>();
|
||||||
|
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -12,22 +12,27 @@ import {
|
|||||||
JSSlot,
|
JSSlot,
|
||||||
NodeSchema,
|
NodeSchema,
|
||||||
NodeData,
|
NodeData,
|
||||||
|
CodePiece,
|
||||||
} from '../types';
|
} from '../types';
|
||||||
import { generateExpression, generateFunction } from './jsExpression';
|
import { generateExpression, generateFunction } from './jsExpression';
|
||||||
|
import { IScopeBindings } from './ScopeBindings';
|
||||||
|
|
||||||
export interface CustomHandlerSet {
|
export interface CustomHandlerSet {
|
||||||
boolean?: (bool: boolean) => string;
|
boolean?(this: CustomHandlerSet, bool: boolean): string;
|
||||||
number?: (num: number) => string;
|
number?(this: CustomHandlerSet, num: number): string;
|
||||||
string?: (str: string) => string;
|
string?(this: CustomHandlerSet, str: string): string;
|
||||||
array?: (arr: JSONArray | CompositeArray) => string;
|
array?(this: CustomHandlerSet, arr: JSONArray | CompositeArray): string;
|
||||||
object?: (obj: JSONObject | CompositeObject) => string;
|
object?(this: CustomHandlerSet, obj: JSONObject | CompositeObject): string;
|
||||||
expression?: (jsExpr: JSExpression) => string;
|
expression?(this: CustomHandlerSet, jsExpr: JSExpression): string;
|
||||||
function?: (jsFunc: JSFunction) => string;
|
function?(this: CustomHandlerSet, jsFunc: JSFunction): string;
|
||||||
slot?: (jsSlot: JSSlot) => string;
|
slot?(this: CustomHandlerSet, jsSlot: JSSlot): string;
|
||||||
node?: (node: NodeSchema) => string;
|
node?(this: CustomHandlerSet, node: NodeSchema): string;
|
||||||
loopDataExpr?: (loopDataExpr: string) => string;
|
nodeAttrs?(this: CustomHandlerSet, node: NodeSchema): CodePiece[];
|
||||||
conditionExpr?: (conditionExpr: string) => string;
|
nodeAttr?(this: CustomHandlerSet, attrName: string, attrValue: CompositeValue): CodePiece[];
|
||||||
tagName?: (tagName: string) => string;
|
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 {
|
function generateArray(value: CompositeArray, handlers: CustomHandlerSet): string {
|
||||||
|
|||||||
239
packages/code-generator/src/utils/expressionParser.ts
Normal file
239
packages/code-generator/src/utils/expressionParser.ts
Normal file
@ -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<keyof (Node & { type: k })>
|
||||||
|
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<string>();
|
||||||
|
|
||||||
|
const ast = parser.parse(`!(${expr});`);
|
||||||
|
|
||||||
|
const addUndeclaredIdentifierIfNeeded = (x: object | null | undefined, path: NodePath<Node>) => {
|
||||||
|
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);
|
||||||
|
}
|
||||||
@ -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') {
|
if (attrName === 'initValue' || attrName === 'labelCol') {
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
const [isString, valueStr] = generateCompositeType(attrValue, handlers);
|
|
||||||
|
const [isString, valueStr] = generateCompositeType(attrValue, customHandlers);
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
value: `${attrName}=${isString ? `"${valueStr}"` : `{${valueStr}}`}`,
|
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;
|
const { props } = nodeItem;
|
||||||
|
|
||||||
let pieces: CodePiece[] = [];
|
let pieces: CodePiece[] = [];
|
||||||
@ -56,12 +70,12 @@ export function generateAttrs(nodeItem: NodeSchema, handlers: CustomHandlerSet):
|
|||||||
if (props) {
|
if (props) {
|
||||||
if (!Array.isArray(props)) {
|
if (!Array.isArray(props)) {
|
||||||
Object.keys(props).forEach((propName: string) => {
|
Object.keys(props).forEach((propName: string) => {
|
||||||
pieces = pieces.concat(generateAttr(propName, props[propName], handlers));
|
pieces = pieces.concat(generateAttr(propName, props[propName], customHandlers));
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
props.forEach((prop) => {
|
props.forEach((prop) => {
|
||||||
if (prop.name && !prop.spread) {
|
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 场景(<Xxx {...(something)}/>)
|
// TODO: 处理 spread 场景(<Xxx {...(something)}/>)
|
||||||
@ -97,10 +111,11 @@ export function generateReactCtrlLine(nodeItem: NodeSchema, handlers: CustomHand
|
|||||||
const loopItemName = nodeItem.loopArgs?.[0] || 'item';
|
const loopItemName = nodeItem.loopArgs?.[0] || 'item';
|
||||||
const loopIndexName = nodeItem.loopArgs?.[1] || 'index';
|
const loopIndexName = nodeItem.loopArgs?.[1] || 'index';
|
||||||
|
|
||||||
// TODO: 静态的值可以抽离出来?
|
const rawLoopDataExpr = isJSExpression(nodeItem.loop)
|
||||||
const loopDataExpr = (handlers.loopDataExpr || _.identity)(
|
? `(${nodeItem.loop.value})`
|
||||||
isJSExpression(nodeItem.loop) ? `(${nodeItem.loop.value})` : `(${JSON.stringify(nodeItem.loop)})`,
|
: `(${JSON.stringify(nodeItem.loop)})`;
|
||||||
);
|
|
||||||
|
const loopDataExpr = handlers.loopDataExpr ? handlers.loopDataExpr(rawLoopDataExpr) : rawLoopDataExpr;
|
||||||
|
|
||||||
pieces.unshift({
|
pieces.unshift({
|
||||||
value: `${loopDataExpr}.map((${loopItemName}, ${loopIndexName}) => (`,
|
value: `${loopDataExpr}.map((${loopItemName}, ${loopIndexName}) => (`,
|
||||||
@ -115,7 +130,8 @@ export function generateReactCtrlLine(nodeItem: NodeSchema, handlers: CustomHand
|
|||||||
|
|
||||||
if (nodeItem.condition) {
|
if (nodeItem.condition) {
|
||||||
const [isString, value] = generateCompositeType(nodeItem.condition, handlers);
|
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({
|
pieces.unshift({
|
||||||
value: `(${conditionExpr}) && (`,
|
value: `(${conditionExpr}) && (`,
|
||||||
@ -149,7 +165,8 @@ export function linkPieces(pieces: CodePiece[], handlers: CustomHandlerSet): str
|
|||||||
throw new CodeGeneratorError('One node only need one tag define');
|
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
|
const beforeParts = pieces
|
||||||
.filter((p) => p.type === PIECE_TYPE.BEFORE)
|
.filter((p) => p.type === PIECE_TYPE.BEFORE)
|
||||||
|
|||||||
@ -10,6 +10,7 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@ali/lowcode-datasource-engine": "^0.1.0",
|
"@ali/lowcode-datasource-engine": "^0.1.0",
|
||||||
|
"universal-env": "^3.2.0",
|
||||||
"rax": "^1.1.0",
|
"rax": "^1.1.0",
|
||||||
"rax-app": "^2.0.0",
|
"rax-app": "^2.0.0",
|
||||||
"rax-document": "^0.1.0",
|
"rax-document": "^0.1.0",
|
||||||
|
|||||||
@ -8,6 +8,8 @@ import Text from 'rax-text';
|
|||||||
|
|
||||||
import { create as __$$createDataSourceEngine } from '@ali/lowcode-datasource-engine';
|
import { create as __$$createDataSourceEngine } from '@ali/lowcode-datasource-engine';
|
||||||
|
|
||||||
|
import { isMiniApp as __$$isMiniApp } from 'universal-env';
|
||||||
|
|
||||||
import __$$projectUtils from '../../utils';
|
import __$$projectUtils from '../../utils';
|
||||||
|
|
||||||
import './index.css';
|
import './index.css';
|
||||||
@ -50,7 +52,7 @@ class Home$$Page extends Component {
|
|||||||
return self._dataSourceEngine.dataSourceMap || {};
|
return self._dataSourceEngine.dataSourceMap || {};
|
||||||
},
|
},
|
||||||
async reloadDataSource() {
|
async reloadDataSource() {
|
||||||
self._dataSourceEngine.reloadDataSource();
|
await self._dataSourceEngine.reloadDataSource();
|
||||||
},
|
},
|
||||||
get utils() {
|
get utils() {
|
||||||
return self._utils;
|
return self._utils;
|
||||||
|
|||||||
@ -10,6 +10,7 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@ali/lowcode-datasource-engine": "^0.1.0",
|
"@ali/lowcode-datasource-engine": "^0.1.0",
|
||||||
|
"universal-env": "^3.2.0",
|
||||||
"rax": "^1.1.0",
|
"rax": "^1.1.0",
|
||||||
"rax-app": "^2.0.0",
|
"rax-app": "^2.0.0",
|
||||||
"rax-document": "^0.1.0",
|
"rax-document": "^0.1.0",
|
||||||
|
|||||||
@ -10,12 +10,15 @@ import Image from 'rax-image';
|
|||||||
|
|
||||||
import { create as __$$createDataSourceEngine } from '@ali/lowcode-datasource-engine';
|
import { create as __$$createDataSourceEngine } from '@ali/lowcode-datasource-engine';
|
||||||
|
|
||||||
|
import { isMiniApp as __$$isMiniApp } from 'universal-env';
|
||||||
|
|
||||||
import __$$projectUtils from '../../utils';
|
import __$$projectUtils from '../../utils';
|
||||||
|
|
||||||
import './index.css';
|
import './index.css';
|
||||||
|
|
||||||
class Home$$Page extends Component {
|
class Home$$Page extends Component {
|
||||||
state = {
|
state = {
|
||||||
|
clickCount: 0,
|
||||||
user: { name: '张三', age: 18, avatar: 'https://gw.alicdn.com/tfs/TB1Ui9BMkY2gK0jSZFgXXc5OFXa-50-50.png' },
|
user: { name: '张三', age: 18, avatar: 'https://gw.alicdn.com/tfs/TB1Ui9BMkY2gK0jSZFgXXc5OFXa-50-50.png' },
|
||||||
orders: [
|
orders: [
|
||||||
{
|
{
|
||||||
@ -61,7 +64,7 @@ class Home$$Page extends Component {
|
|||||||
source={{ uri: __$$eval(() => __$$context.state.user.avatar) }}
|
source={{ uri: __$$eval(() => __$$context.state.user.avatar) }}
|
||||||
style={{ width: '32px', height: '32px' }}
|
style={{ width: '32px', height: '32px' }}
|
||||||
/>
|
/>
|
||||||
<View onClick={__$$eval(() => __$$context.hello)}>
|
<View onClick={__$$context.hello}>
|
||||||
<Text>{__$$eval(() => __$$context.state.user.name)}</Text>
|
<Text>{__$$eval(() => __$$context.state.user.name)}</Text>
|
||||||
<Text>{__$$eval(() => __$$context.state.user.age)}岁</Text>
|
<Text>{__$$eval(() => __$$context.state.user.age)}岁</Text>
|
||||||
</View>
|
</View>
|
||||||
@ -70,29 +73,47 @@ class Home$$Page extends Component {
|
|||||||
<View>
|
<View>
|
||||||
<Text>=== Orders: ===</Text>
|
<Text>=== Orders: ===</Text>
|
||||||
</View>
|
</View>
|
||||||
{__$$evalArray(() => __$$context.state.orders).map((item, index) => (
|
{__$$evalArray(() => __$$context.state.orders).map((order, index) => (
|
||||||
<View
|
<View
|
||||||
style={{ flexDirection: 'row' }}
|
style={{ flexDirection: 'row' }}
|
||||||
onClick={__$$eval(
|
data-order={order}
|
||||||
() =>
|
onClick={(...__$$args) => {
|
||||||
function () {
|
if (__$$isMiniApp) {
|
||||||
__$$context.utils.recordEvent(`CLICK_ORDER`, item.title);
|
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);
|
||||||
|
}
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
<View>
|
<View>
|
||||||
<Image source={{ uri: __$$eval(() => item.coverUrl) }} style={{ width: '80px', height: '60px' }} />
|
<Image source={{ uri: __$$eval(() => order.coverUrl) }} style={{ width: '80px', height: '60px' }} />
|
||||||
</View>
|
</View>
|
||||||
<View>
|
<View>
|
||||||
<Text>{__$$eval(() => item.title)}</Text>
|
<Text>{__$$eval(() => order.title)}</Text>
|
||||||
<Text>{__$$eval(() => __$$context.utils.formatPrice(item.price, '元'))}</Text>
|
<Text>{__$$eval(() => __$$context.utils.formatPrice(order.price, '元'))}</Text>
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
))}
|
))}
|
||||||
|
<View
|
||||||
|
onClick={function () {
|
||||||
|
__$$context.setState({
|
||||||
|
clickCount: __$$context.state.clickCount + 1,
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Text>点击次数:{__$$eval(() => __$$context.state.clickCount)}(点击加 1)</Text>
|
||||||
|
</View>
|
||||||
<View>
|
<View>
|
||||||
<Text>操作提示:</Text>
|
<Text>操作提示:</Text>
|
||||||
<Text>1. 点击会员名,可以弹出 Toast "Hello xxx!"</Text>
|
<Text>1. 点击会员名,可以弹出 Toast "Hello xxx!"</Text>
|
||||||
<Text>2. 点击订单,会记录点击的订单信息,并弹出 Toast 提示</Text>
|
<Text>2. 点击订单,会记录点击的订单信息,并弹出 Toast 提示</Text>
|
||||||
|
<Text>3. 最下面的【点击次数】,点一次应该加 1</Text>
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
);
|
);
|
||||||
@ -112,7 +133,7 @@ class Home$$Page extends Component {
|
|||||||
return self._dataSourceEngine.dataSourceMap || {};
|
return self._dataSourceEngine.dataSourceMap || {};
|
||||||
},
|
},
|
||||||
async reloadDataSource() {
|
async reloadDataSource() {
|
||||||
self._dataSourceEngine.reloadDataSource();
|
await self._dataSourceEngine.reloadDataSource();
|
||||||
},
|
},
|
||||||
get utils() {
|
get utils() {
|
||||||
return self._utils;
|
return self._utils;
|
||||||
|
|||||||
@ -36,6 +36,7 @@
|
|||||||
componentName: 'Page',
|
componentName: 'Page',
|
||||||
fileName: 'home',
|
fileName: 'home',
|
||||||
state: {
|
state: {
|
||||||
|
clickCount: 0,
|
||||||
user: { name: '张三', age: 18, avatar: 'https://gw.alicdn.com/tfs/TB1Ui9BMkY2gK0jSZFgXXc5OFXa-50-50.png' },
|
user: { name: '张三', age: 18, avatar: 'https://gw.alicdn.com/tfs/TB1Ui9BMkY2gK0jSZFgXXc5OFXa-50-50.png' },
|
||||||
orders: [
|
orders: [
|
||||||
{
|
{
|
||||||
@ -176,11 +177,12 @@
|
|||||||
type: 'JSExpression',
|
type: 'JSExpression',
|
||||||
value: 'this.state.orders',
|
value: 'this.state.orders',
|
||||||
},
|
},
|
||||||
|
loopArgs: ['order', 'index'],
|
||||||
props: {
|
props: {
|
||||||
style: { flexDirection: 'row' },
|
style: { flexDirection: 'row' },
|
||||||
onClick: {
|
onClick: {
|
||||||
type: 'JSFunction',
|
type: 'JSFunction',
|
||||||
value: 'function(){ this.utils.recordEvent(`CLICK_ORDER`, this.item.title) }',
|
value: 'function(){ this.utils.recordEvent(`CLICK_ORDER`, this.order.title) }',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
children: [
|
children: [
|
||||||
@ -193,7 +195,7 @@
|
|||||||
source: {
|
source: {
|
||||||
uri: {
|
uri: {
|
||||||
type: 'JSExpression',
|
type: 'JSExpression',
|
||||||
value: 'this.item.coverUrl',
|
value: 'this.order.coverUrl',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
style: {
|
style: {
|
||||||
@ -211,20 +213,42 @@
|
|||||||
componentName: 'Text',
|
componentName: 'Text',
|
||||||
children: {
|
children: {
|
||||||
type: 'JSExpression',
|
type: 'JSExpression',
|
||||||
value: 'this.item.title',
|
value: 'this.order.title',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
componentName: 'Text',
|
componentName: 'Text',
|
||||||
children: {
|
children: {
|
||||||
type: 'JSExpression',
|
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',
|
componentName: 'View',
|
||||||
children: [
|
children: [
|
||||||
@ -243,6 +267,11 @@
|
|||||||
props: {},
|
props: {},
|
||||||
children: '2. 点击订单,会记录点击的订单信息,并弹出 Toast 提示',
|
children: '2. 点击订单,会记录点击的订单信息,并弹出 Toast 提示',
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
componentName: 'Text',
|
||||||
|
props: {},
|
||||||
|
children: '3. 最下面的【点击次数】,点一次应该加 1',
|
||||||
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
|||||||
@ -0,0 +1,79 @@
|
|||||||
|
import test from 'ava';
|
||||||
|
import type { ExecutionContext, Macro } from 'ava';
|
||||||
|
import { parseExpressionConvertThis2Context } from '../../../src/utils/expressionParser';
|
||||||
|
|
||||||
|
const macro: Macro<any[]> = (
|
||||||
|
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}',
|
||||||
|
);
|
||||||
@ -0,0 +1,114 @@
|
|||||||
|
import test from 'ava';
|
||||||
|
import type { ExecutionContext, Macro } from 'ava';
|
||||||
|
import {
|
||||||
|
parseExpressionGetGlobalVariables,
|
||||||
|
ParseExpressionGetGlobalVariablesOptions,
|
||||||
|
} from '../../../src/utils/expressionParser';
|
||||||
|
|
||||||
|
const macro: Macro<any[]> = (
|
||||||
|
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: 补充更多类型的测试用例
|
||||||
Loading…
x
Reference in New Issue
Block a user