fix: 🐛 解决 react 中 jsx 出码的时候对于循环数据漏包 __$evalArray 的问题

This commit is contained in:
Clarence-pan 2022-04-11 12:23:03 +08:00 committed by Clarence Pan
parent ee65b8fa17
commit 3b9b177b05
20 changed files with 337 additions and 36 deletions

View File

@ -75,6 +75,7 @@
"change-case": "^3.1.0",
"commander": "^6.1.0",
"debug": "^4.3.2",
"fp-ts": "^2.11.9",
"fs-extra": "9.x",
"glob": "^7.2.0",
"html-entities": "^2.3.2",

View File

@ -83,6 +83,23 @@ const pluginFactory: BuilderComponentPluginFactory<PluginConfig> = (config?) =>
`,
linkAfter: [...DEFAULT_LINK_AFTER[CLASS_DEFINE_CHUNK_NAME.InsMethod]],
});
} else {
// useRef 为 false 的时候是指没有组件在 props 中配置 ref 属性,但这个时候其实也可能有代码访问 this.$/$$ 所以还是加上个空的代码
next.chunks.push({
type: ChunkType.STRING,
fileType: cfg.fileType,
name: CLASS_DEFINE_CHUNK_NAME.InsMethod,
content: ` $ = () => null; `,
linkAfter: [...DEFAULT_LINK_AFTER[CLASS_DEFINE_CHUNK_NAME.InsMethod]],
});
next.chunks.push({
type: ChunkType.STRING,
fileType: cfg.fileType,
name: CLASS_DEFINE_CHUNK_NAME.InsMethod,
content: ` $$ = () => []; `,
linkAfter: [...DEFAULT_LINK_AFTER[CLASS_DEFINE_CHUNK_NAME.InsMethod]],
});
}
return next;

View File

@ -47,9 +47,9 @@ const pluginFactory: BuilderComponentPluginFactory<PluginConfig> = (config?) =>
// 这里会将内部的一些子上下文的访问(this.xxx)转换为 __$$context.xxx 的形式
// 与 Rax 所不同的是,这里不会将最顶层的 this 转换掉
const customHandlers: HandlerSet<string> = {
expression(input: JSExpression, scope: IScope) {
expression(input: JSExpression, scope: IScope, config) {
return transformJsExpr(generateExpression(input, scope), scope, {
dontWrapEval: !tolerateEvalErrors,
dontWrapEval: !(config?.tolerateEvalErrors ?? tolerateEvalErrors),
dontTransformThis2ContextAtRootScope: true,
});
},
@ -71,6 +71,7 @@ const pluginFactory: BuilderComponentPluginFactory<PluginConfig> = (config?) =>
const generatorPlugins: NodeGeneratorConfig = {
handlers: customHandlers,
tagMapping: (v) => nodeTypeMapping[v] || v,
tolerateEvalErrors,
};
if (next.contextData.useRefApi) {

View File

@ -217,6 +217,7 @@ export interface HandlerSet<T> {
export interface CompositeValueGeneratorOptions {
handlers?: HandlerSet<string>;
nodeGenerator?: NodeGenerator<string>;
tolerateEvalErrors?: boolean;
}
/**

View File

@ -15,7 +15,10 @@ export interface CodePiece {
type: PIECE_TYPE;
}
export interface AttrData { attrName: string; attrValue: CompositeValue }
export interface AttrData {
attrName: string;
attrValue: CompositeValue;
}
// 对 JSX 出码的理解,目前定制点包含 【包装】【标签名】【属性】
export type AttrPlugin = BaseGenerator<AttrData, CodePiece[], NodeGeneratorConfig>;
export type NodePlugin = BaseGenerator<NodeSchema, CodePiece[], NodeGeneratorConfig>;
@ -26,4 +29,12 @@ export interface NodeGeneratorConfig {
attrPlugins?: AttrPlugin[];
nodePlugins?: NodePlugin[];
self?: NodeGenerator<string>;
/**
* JSExpression
* true
* : 如果容忍异常 try-catch -- __$$eval / __$$evalArray
* catch CustomEvent
*/
tolerateEvalErrors?: boolean;
}

View File

@ -79,10 +79,11 @@ export function isJsCode(value: unknown): boolean {
export function generateExpression(value: any, scope: IScope): string {
if (isJSExpression(value)) {
const exprVal = (value as JSExpression).value;
const exprVal = (value as JSExpression).value.trim();
if (!exprVal) {
return 'null';
}
const afterProcessWithLocals = transformExpressionLocalRef(exprVal, scope);
return afterProcessWithLocals;
}

View File

@ -1,4 +1,5 @@
import _ from 'lodash';
import { pipe } from 'fp-ts/function';
import { NodeSchema, isNodeSchema, NodeDataType, CompositeValue } from '@alilc/lowcode-types';
import {
@ -17,6 +18,7 @@ import { getStaticExprValue } from './common';
import { executeFunctionStack } from './aopHelper';
import { encodeJsxStringNode } from './encodeJsxAttrString';
import { unwrapJsExprQuoteInJsx } from './jsxHelpers';
import { transformThis2Context } from '../core/jsx/handlers/transformThis2Context';
function mergeNodeGeneratorConfig(
cfg1: NodeGeneratorConfig,
@ -255,6 +257,8 @@ export function generateReactLoopCtrl(
next?: NodePlugin,
): CodePiece[] {
if (nodeItem.loop) {
const tolerateEvalErrors = config?.tolerateEvalErrors ?? true;
const loopItemName = nodeItem.loopArgs?.[0] || 'item';
const loopIndexName = nodeItem.loopArgs?.[1] || 'index';
@ -262,9 +266,20 @@ export function generateReactLoopCtrl(
const subScope = scope.createSubScope([loopItemName, loopIndexName]);
const pieces: CodePiece[] = next ? next(nodeItem, subScope, config) : [];
const loopDataExpr = generateCompositeType(nodeItem.loop, scope, {
handlers: config?.handlers,
});
// 生成循环变量表达式
const loopDataExpr = pipe(
nodeItem.loop,
// 将 JSExpression 转换为 JS 表达式代码:
(expr) =>
generateCompositeType(expr, scope, {
handlers: config?.handlers,
tolerateEvalErrors: false, // 这个内部不需要包 try catch, 下面会统一加的
}),
// 将 this.xxx 转换为 __$$context.xxx:
(expr) => transformThis2Context(expr, scope, { ignoreRootScope: true }),
// 如果要容忍错误,则包一层 try catch (基于助手函数 __$$evalArray)
(expr) => (tolerateEvalErrors ? `__$$evalArray(() => (${expr}))` : expr),
);
pieces.unshift({
value: `(${loopDataExpr}).map((${loopItemName}, ${loopIndexName}) => ((__$$context) => (`,

View File

@ -152,7 +152,7 @@ class Test$$Page extends React.Component {
</Form.Item>
<div style={{ textAlign: "center" }}>
<Button.Group>
{["a", "b", "c"].map((item, index) =>
{__$$evalArray(() => ["a", "b", "c"]).map((item, index) =>
((__$$context) =>
!!__$$eval(() => index >= 1) && (
<Button type="primary" style={{ margin: "0 5px 0 5px" }}>

View File

@ -45,6 +45,10 @@ class Aaaa$$Page extends React.Component {
this.state = {};
}
$ = () => null;
$$ = () => [];
_defineDataSourceConfig() {
const _this = this;
return {

View File

@ -38,6 +38,10 @@ class Test$$Page extends React.Component {
this.state = {};
}
$ = () => null;
$$ = () => [];
componentDidMount() {}
render() {

View File

@ -152,7 +152,7 @@ class Test$$Page extends React.Component {
</Form.Item>
<div style={{ textAlign: "center" }}>
<Button.Group>
{["a", "b", "c"].map((item, index) =>
{__$$evalArray(() => ["a", "b", "c"]).map((item, index) =>
((__$$context) =>
!!false && (
<Button type="primary" style={{ margin: "0 5px 0 5px" }}>

View File

@ -41,6 +41,10 @@ class Example$$Page extends React.Component {
this.state = {};
}
$ = () => null;
$$ = () => [];
_defineDataSourceConfig() {
const _this = this;
return {

View File

@ -41,6 +41,10 @@ class Index$$Page extends React.Component {
this.state = {};
}
$ = () => null;
$$ = () => [];
_defineDataSourceConfig() {
const _this = this;
return {
@ -75,16 +79,17 @@ class Index$$Page extends React.Component {
return (
<div>
<div>
{__$$eval(() => this.dataSourceMap.todos.data).map((item, index) =>
((__$$context) => (
<div>
<Switch
checkedChildren="开"
unCheckedChildren="关"
checked={__$$eval(() => item.done)}
/>
</div>
))(__$$createChildContext(__$$context, { item, index }))
{__$$evalArray(() => this.dataSourceMap.todos.data).map(
(item, index) =>
((__$$context) => (
<div>
<Switch
checkedChildren="开"
unCheckedChildren="关"
checked={__$$eval(() => item.done)}
/>
</div>
))(__$$createChildContext(__$$context, { item, index }))
)}
</div>
</div>

View File

@ -238,7 +238,7 @@ class Test$$Page extends React.Component {
width="720px"
centered={true}
>
{__$$eval(() => this.state.results).map((item, index) =>
{__$$evalArray(() => this.state.results).map((item, index) =>
((__$$context) => (
<AliAutoDivDefault style={{ width: "100%" }}>
{!!__$$eval(
@ -441,7 +441,7 @@ class Test$$Page extends React.Component {
width: 142,
render: (text, record, index) =>
((__$$context) =>
__$$eval(() => text.split(",")).map(
__$$evalArray(() => text.split(",")).map(
(item, index) =>
((__$$context) => (
<Typography.Text
@ -470,7 +470,7 @@ class Test$$Page extends React.Component {
render: (text, record, index) =>
((__$$context) => (
<Tooltip
title={__$$eval(() => text || []).map(
title={__$$evalArray(() => text || []).map(
(item, index) =>
((__$$context) => (
<Typography.Text

View File

@ -303,7 +303,7 @@ class Test$$Page extends React.Component {
</Button>
</AliAutoDivDefault>
)}
{__$$eval(() => this.state.results).map((item, index) =>
{__$$evalArray(() => this.state.results).map((item, index) =>
((__$$context) => (
<AliAutoDivDefault style={{ width: "100%", marginTop: "10px" }}>
<Typography.Text>
@ -595,17 +595,18 @@ class Test$$Page extends React.Component {
width: 142,
render: (text, record, index) =>
((__$$context) =>
__$$eval(() => text.split(",")).map((item, index) =>
((__$$context) => (
<Typography.Text style={{ display: "block" }}>
{__$$eval(() => item)}
</Typography.Text>
))(
__$$createChildContext(__$$context, {
item,
index,
})
)
__$$evalArray(() => text.split(",")).map(
(item, index) =>
((__$$context) => (
<Typography.Text style={{ display: "block" }}>
{__$$eval(() => item)}
</Typography.Text>
))(
__$$createChildContext(__$$context, {
item,
index,
})
)
))(
__$$createChildContext(__$$context, {
text,
@ -621,7 +622,7 @@ class Test$$Page extends React.Component {
render: (text, record, index) =>
((__$$context) => (
<Tooltip
title={__$$eval(() => text || []).map(
title={__$$evalArray(() => text || []).map(
(item, index) =>
((__$$context) => (
<Typography.Text

View File

@ -0,0 +1,58 @@
{
"version": "1.0.0",
"componentsMap": [
{
"package": "react-greetings",
"version": "1.0.0",
"componentName": "Greetings",
"exportName": "Greetings",
"destructuring": true
}
],
"componentsTree": [
{
"componentName": "Page",
"id": "node_ocl137q7oc1",
"fileName": "test",
"props": { "style": {} },
"lifeCycles": {},
"dataSource": { "list": [] },
"state": {
"name": "lowcode world",
"users": null
},
"methods": {},
"children": [
{
"componentName": "Greetings",
"id": "node_ocl137q7oc4",
"loop": {
"type": "JSExpression",
"value": "this.state.users"
},
"loopArgs": ["item", ""],
"props": {
"content": {
"type": "i18n",
"key": "greetings.hello",
"params": {
"name": {
"type": "JSExpression",
"value": "this.item"
}
}
}
}
}
]
}
],
"i18n": {
"zh_CN": {
"greetings.hello": "${name}, 你好!"
},
"en_US": {
"greetings.hello": "Hello, ${name}!"
}
}
}

View File

@ -0,0 +1,52 @@
import CodeGenerator from '../../src';
import * as fs from 'fs';
import * as path from 'path';
import { ProjectSchema } from '@alilc/lowcode-types';
import { createDiskPublisher } from '../helpers/solutionHelper';
import { IceJsProjectBuilderOptions } from '../../src/solutions/icejs';
const testCaseBaseName = path.basename(__filename, '.test.ts');
const inputSchemaJsonFile = path.join(__dirname, `${testCaseBaseName}.schema.json`);
const outputDir = path.join(__dirname, `${testCaseBaseName}.generated`);
jest.setTimeout(60 * 60 * 1000);
test('loop should be generated link __$$evalArray(xxx).map', async () => {
await exportProject(
inputSchemaJsonFile,
outputDir,
{},
{ inStrictMode: true, tolerateEvalErrors: true },
);
const generatedPageFileContent = readOutputTextFile('demo-project/src/pages/Test/index.jsx');
expect(generatedPageFileContent).toContain(
'{__$$evalArray(() => this.state.users).map((item, index) =>',
);
});
function exportProject(
importPath: string,
outputPath: string,
mergeSchema?: Partial<ProjectSchema>,
options?: IceJsProjectBuilderOptions,
) {
const schemaJsonStr = fs.readFileSync(importPath, { encoding: 'utf8' });
const schema = { ...JSON.parse(schemaJsonStr), ...mergeSchema };
const builder = CodeGenerator.solutions.icejs(options);
return builder.generateProject(schema).then(async (result) => {
const publisher = createDiskPublisher();
await publisher.publish({
project: result,
outputPath,
projectSlug: 'demo-project',
createProjectFolder: true,
});
return result;
});
}
function readOutputTextFile(outputFilePath: string): string {
return fs.readFileSync(path.resolve(outputDir, outputFilePath), 'utf-8');
}

View File

@ -0,0 +1,70 @@
{
"version": "1.0.0",
"componentsMap": [
{
"package": "react-greetings",
"version": "1.0.0",
"componentName": "Greetings",
"exportName": "Greetings",
"destructuring": true
}
],
"componentsTree": [
{
"componentName": "Page",
"id": "node_ocl137q7oc1",
"fileName": "test",
"props": { "style": {} },
"lifeCycles": {},
"dataSource": { "list": [] },
"state": {
"name": "lowcode world",
"users": null
},
"methods": {},
"children": [
{
"componentName": "Greetings",
"id": "node_ocl137q7oc4",
"loop": {
"type": "JSExpression",
"value": "this.state.users"
},
"loopArgs": ["item", ""],
"props": {
"content": {
"type": "i18n",
"key": "greetings.hello",
"params": { "name": { "type": "JSExpression", "value": "this.item" } }
}
},
"children": [
{
"componentName": "Greetings",
"id": "node_ocl137q7oc4",
"loop": {
"type": "JSExpression",
"value": "this.state.messages"
},
"loopArgs": ["msg", ""],
"props": {
"content": {
"type": "JSExpression",
"value": "this.msg"
}
}
}
]
}
]
}
],
"i18n": {
"zh_CN": {
"greetings.hello": "${name}, 你好!"
},
"en_US": {
"greetings.hello": "Hello, ${name}!"
}
}
}

View File

@ -0,0 +1,56 @@
import CodeGenerator from '../../src';
import * as fs from 'fs';
import * as path from 'path';
import { ProjectSchema } from '@alilc/lowcode-types';
import { createDiskPublisher } from '../helpers/solutionHelper';
import { IceJsProjectBuilderOptions } from '../../src/solutions/icejs';
const testCaseBaseName = path.basename(__filename, '.test.ts');
const inputSchemaJsonFile = path.join(__dirname, `${testCaseBaseName}.schema.json`);
const outputDir = path.join(__dirname, `${testCaseBaseName}.generated`);
jest.setTimeout(60 * 60 * 1000);
test('loop should be generated link __$$evalArray(xxx).map', async () => {
await exportProject(
inputSchemaJsonFile,
outputDir,
{},
{ inStrictMode: true, tolerateEvalErrors: true },
);
const generatedPageFileContent = readOutputTextFile('demo-project/src/pages/Test/index.jsx');
expect(generatedPageFileContent).toContain(
'{__$$evalArray(() => this.state.users).map((item, index) =>',
);
expect(generatedPageFileContent).toContain(
'{__$$evalArray(() => __$$context.state.messages).map(',
);
});
function exportProject(
importPath: string,
outputPath: string,
mergeSchema?: Partial<ProjectSchema>,
options?: IceJsProjectBuilderOptions,
) {
const schemaJsonStr = fs.readFileSync(importPath, { encoding: 'utf8' });
const schema = { ...JSON.parse(schemaJsonStr), ...mergeSchema };
const builder = CodeGenerator.solutions.icejs(options);
return builder.generateProject(schema).then(async (result) => {
const publisher = createDiskPublisher();
await publisher.publish({
project: result,
outputPath,
projectSlug: 'demo-project',
createProjectFolder: true,
});
return result;
});
}
function readOutputTextFile(outputFilePath: string): string {
return fs.readFileSync(path.resolve(outputDir, outputFilePath), 'utf-8');
}

View File

@ -7,7 +7,7 @@ Object {
"content": "
const __$$context = this._context || this;
const { state } = __$$context;
return (__$$eval(() => (this.state.otherThings))).map((item, index) => ((__$$context) => (!!(__$$eval(() => (__$$context.state.something))) && (<Page><Text>Hello world!</Text></Page>)))(__$$createChildContext(__$$context, { item, index })));
return (__$$evalArray(() => (this.state.otherThings))).map((item, index) => ((__$$context) => (!!(__$$eval(() => (__$$context.state.something))) && (<Page><Text>Hello world!</Text></Page>)))(__$$createChildContext(__$$context, { item, index })));
",
"fileType": "jsx",
"linkAfter": Array [