diff --git a/jest.config.js b/jest.config.js index 240c494..11a07d1 100644 --- a/jest.config.js +++ b/jest.config.js @@ -154,7 +154,7 @@ const config = { // testLocationInResults: false, // The glob patterns Jest uses to detect test files - testMatch: ['**/__tests__/**/*.[jt]s?(x)', '**/?(*.)+(spec|test).[tj]s?(x)'], + testMatch: ['**/tests/**/*.[jt]s?(x)', '**/?(*.)+(spec|test).[tj]s?(x)'], // An array of regexp pattern strings that are matched against all test paths, matched tests are skipped // testPathIgnorePatterns: [ diff --git a/packages/core/src/helpers/assert.ts b/packages/core/src/helpers/assert.ts index bf30a5d..c21c0f4 100644 --- a/packages/core/src/helpers/assert.ts +++ b/packages/core/src/helpers/assert.ts @@ -1,3 +1,5 @@ +import { isValidExpressionCode } from './ast'; + const defineServiceHandlerNames = ['defineServices', 'createServices']; const sfHandlerPattern = new RegExp(`^(${defineServiceHandlerNames.join('|')})$`); @@ -32,3 +34,16 @@ export function isDefineStore(name: string) { export function isTangoVariable(name: string) { return /^tango\??\.(stores|services)\??\./.test(name) && name.split('.').length > 2; } + +const templatePattern = /^{(.+)}$/s; + +/** + * 判断给定字符串是否被表达式容器`{expCode}`包裹 + * @param code + */ +export function isWrappedByExpressionContainer(code: string, isStrict = true) { + if (isStrict && isValidExpressionCode(code)) { + return false; + } + return templatePattern.test(code); +} diff --git a/packages/core/src/helpers/ast/parse.ts b/packages/core/src/helpers/ast/parse.ts index 41d9fd8..1fea68e 100644 --- a/packages/core/src/helpers/ast/parse.ts +++ b/packages/core/src/helpers/ast/parse.ts @@ -6,10 +6,10 @@ import * as t from '@babel/types'; import { logger, isValidObjectString, - isVariableString, getVariableContent, isPlainObject, } from '@music163/tango-helpers'; +import { isWrappedByExpressionContainer } from '../assert'; // @see https://babeljs.io/docs/en/babel-parser#pluginss const babelParserConfig: ParserOptions = { @@ -22,7 +22,6 @@ const babelParserConfig: ParserOptions = { 'classProperties', 'asyncGenerators', 'functionBind', - 'functionSent', 'dynamicImport', 'optionalChaining', ], @@ -44,6 +43,7 @@ export function isValidCode(code: string) { /** * 检测代码是否是合法的表达式代码 + * 表达式是一组代码的集合,它返回一个值;每一个合法的表达式都能计算成某个值 * @param code * @returns */ @@ -91,8 +91,8 @@ export function code2expression(code: string) { try { expNode = t.cloneNode(parseExpression(code, babelParserConfig), false, true); } catch (err) { - console.error('invalid code', err); - expNode = t.identifier('undefined'); + logger.error('invalid code', err); + // expNode = t.identifier('undefined'); } return expNode; } @@ -103,7 +103,7 @@ export function code2expression(code: string) { * @returns File */ export function expressionCode2ast(code: string) { - if (isVariableString(code)) { + if (isWrappedByExpressionContainer(code)) { code = getVariableContent(code); } const node = code2expression(code); @@ -130,7 +130,7 @@ export function value2node( ret = t.numericLiteral(value); break; case 'string': - if (isVariableString(value)) { + if (isWrappedByExpressionContainer(value)) { // 再检查是否是表达式容器,例如 {this.foo}, {1} const innerString = getVariableContent(value); ret = code2expression(innerString); @@ -201,7 +201,7 @@ export function value2jsxAttributeValueNode(value: any) { if (isValidObjectString(value)) { // 先检查是否是对象字符串 ret = t.jsxExpressionContainer(code2expression(value)); - } else if (isVariableString(value)) { + } else if (isWrappedByExpressionContainer(value)) { // 再检查是否是表达式容器,例如 {this.foo}, {1} const innerString = getVariableContent(value); ret = t.jsxExpressionContainer(code2expression(innerString)); @@ -221,7 +221,7 @@ export function value2jsxChildrenValueNode(value: any) { let ret: t.JSXElement | t.JSXFragment | t.JSXExpressionContainer | t.JSXSpreadChild | t.JSXText; switch (typeof value) { case 'string': - if (isVariableString(value)) { + if (isWrappedByExpressionContainer(value)) { const innerString = getVariableContent(value); ret = t.jsxExpressionContainer(code2expression(innerString)); } else { diff --git a/packages/core/src/helpers/code-helpers.ts b/packages/core/src/helpers/code-helpers.ts index 82f0205..68f9b37 100644 --- a/packages/core/src/helpers/code-helpers.ts +++ b/packages/core/src/helpers/code-helpers.ts @@ -1,5 +1,6 @@ -import { getVariableContent, isVariableString } from '@music163/tango-helpers'; -import { value2node, expression2code } from './ast'; +import { getVariableContent } from '@music163/tango-helpers'; +import { value2node, expression2code, isValidExpressionCode } from './ast'; +import { isWrappedByExpressionContainer } from './assert'; /** * 将 js value 转换为代码字符串 @@ -36,7 +37,9 @@ export function value2expressionCode(val: any) { switch (typeof val) { case 'string': { - if (isVariableString(val)) { + if (isValidExpressionCode(val)) { + ret = val; + } else if (isWrappedByExpressionContainer(val, false)) { ret = getVariableContent(val); } else if (isStringCode(val)) { ret = val; diff --git a/packages/core/src/helpers/prototype.ts b/packages/core/src/helpers/prototype.ts index 627a8b8..ec1e2fc 100644 --- a/packages/core/src/helpers/prototype.ts +++ b/packages/core/src/helpers/prototype.ts @@ -3,13 +3,13 @@ import { ComponentPropType, ComponentPrototypeType, isNil, - isVariableString, logger, uuid, } from '@music163/tango-helpers'; import { getRelativePath, isFilepath } from './string'; import type { IImportDeclarationPayload } from '../types'; import { code2expression } from './ast'; +import { isWrappedByExpressionContainer } from './assert'; /** * 根据组件的 prototype 生成 ImportDeclarationPayload @@ -82,7 +82,7 @@ function getPropKeyValuePair(item: ComponentPropType, generateValue: (...args: a break; } case 'string': { - if (!isVariableString(value)) { + if (!isWrappedByExpressionContainer(value)) { // 不是变量字符串 value = `"${value}"`; } else { diff --git a/packages/core/src/models/interfaces.ts b/packages/core/src/models/interfaces.ts index 74f4ad9..e7f24bc 100644 --- a/packages/core/src/models/interfaces.ts +++ b/packages/core/src/models/interfaces.ts @@ -208,15 +208,15 @@ export interface IWorkspace { // ----------------- 服务函数文件操作 ----------------- - getServiceFunction: (serviceKey: string) => object; - listServiceFunctions: () => Record; - removeServiceFunction: (serviceName: string, modName?: string) => void; - addServiceFunction: ( + getServiceFunction?: (serviceKey: string) => object; + listServiceFunctions?: () => Record; + removeServiceFunction?: (serviceName: string, modName?: string) => void; + addServiceFunction?: ( payload: IServiceFunctionPayload | IServiceFunctionPayload[], modName?: string, ) => void; - updateServiceFunction: (payload: IServiceFunctionPayload, modName?: string) => void; - updateServiceBaseConfig: (IServiceFunctionPayload: object, modName?: string) => void; + updateServiceFunction?: (payload: IServiceFunctionPayload, modName?: string) => void; + updateServiceBaseConfig?: (IServiceFunctionPayload: object, modName?: string) => void; // ----------------- 状态管理文件操作 ----------------- addStoreState?: (storeName: string, stateName: string, initValue: string) => void; @@ -234,11 +234,11 @@ export interface IWorkspace { // ----------------- 依赖包操作 ----------------- - addDependency: (data: any) => void; - listDependencies: () => any; - getDependency: (pkgName: string) => object; + addDependency?: (data: any) => void; + listDependencies?: () => any; + getDependency?: (pkgName: string) => object; - updateDependency: ( + updateDependency?: ( name: string, version: string, options?: { @@ -247,7 +247,7 @@ export interface IWorkspace { }, ) => void; - removeDependency: (name: string) => void; + removeDependency?: (name: string) => void; addBizComp?: ( name: string, diff --git a/packages/core/src/models/workspace.ts b/packages/core/src/models/workspace.ts index cd08118..356e568 100644 --- a/packages/core/src/models/workspace.ts +++ b/packages/core/src/models/workspace.ts @@ -527,7 +527,8 @@ export class Workspace extends EventTarget implements IWorkspace { /** * 添加新的视图文件 - * @deprecated + * @deprecated 使用 addViewFile 代替 + * FIXME: 重构这个逻辑 * @param route 视图名 * @param code 视图代码 */ diff --git a/packages/core/tests/assert.test.ts b/packages/core/tests/assert.test.ts new file mode 100644 index 0000000..d9fb885 --- /dev/null +++ b/packages/core/tests/assert.test.ts @@ -0,0 +1,25 @@ +import { isTangoVariable, isWrappedByExpressionContainer } from '../src/helpers'; + +describe('assert', () => { + it('isTangoVariable', () => { + expect(isTangoVariable('tango.stores.app.name')).toBeTruthy(); + expect(isTangoVariable('tango.stores?.app?.name')).toBeTruthy(); + expect(isTangoVariable('tango.stores.app?.name')).toBeTruthy(); + // expect(isTangoVariable('tango.copyToClipboard')).toBeTruthy(); + }); + + it('isWrappedByExpressionContainer', () => { + expect(isWrappedByExpressionContainer('{this.foo}')).toBeTruthy(); + expect(isWrappedByExpressionContainer('{!false}')).toBeTruthy(); + expect(isWrappedByExpressionContainer('{[]}')).toBeTruthy(); + expect(isWrappedByExpressionContainer('{{ foo: "bar" }}')).toBeTruthy(); + expect(isWrappedByExpressionContainer('{[{ foo: "bar" }]}')).toBeTruthy(); + expect(isWrappedByExpressionContainer('{123}')).toBeTruthy(); + expect(isWrappedByExpressionContainer('{"hello"}')).toBeTruthy(); + expect(isWrappedByExpressionContainer('{ foo: "bar" }')).toBeFalsy(); + expect(isWrappedByExpressionContainer('{ type: tango.stores?.homePage?.tabKey }')).toBeFalsy(); + expect(isWrappedByExpressionContainer('{ type: tango.stores.homePage.tabKey }')).toBeFalsy(); + expect(isWrappedByExpressionContainer('{ foo: "bar" }')).toBeFalsy(); + expect(isWrappedByExpressionContainer('{ color: tango.stores.app.color }')).toBeFalsy(); + }); +}); diff --git a/packages/core/tests/ast.test.ts b/packages/core/tests/ast.test.ts index 9d7b360..95afc94 100644 --- a/packages/core/tests/ast.test.ts +++ b/packages/core/tests/ast.test.ts @@ -3,15 +3,16 @@ import { serviceConfig2Node, isValidCode, isValidExpressionCode, + code2expression, } from '../src/helpers'; -describe('helpers', () => { - test('isValidCode', () => { +describe('ast helpers', () => { + it('isValidCode', () => { expect(isValidCode('() => { hello world }')).toBeFalsy(); expect(isValidCode('function() {}')).toBeFalsy(); }); - test('isValidExpression', () => { + it('isValidExpression', () => { expect(isValidExpressionCode('() => { }')).toBeTruthy(); expect(isValidExpressionCode('1')).toBeTruthy(); expect(isValidExpressionCode('"hello"')).toBeTruthy(); @@ -20,9 +21,17 @@ describe('helpers', () => { expect(isValidExpressionCode('[1,2,3]')).toBeTruthy(); expect(isValidExpressionCode('
hello
')).toBeTruthy(); expect(isValidExpressionCode('
hello
')).toBeTruthy(); + + expect(isValidExpressionCode('{1}')).toBeFalsy(); + expect(isValidExpressionCode('{"1"}')).toBeFalsy(); + expect(isValidExpressionCode('{ 1+1 }')).toBeFalsy(); + expect(isValidExpressionCode('{
aaa
}')).toBeFalsy(); + expect(isValidExpressionCode('{() => {}}')).toBeFalsy(); + expect(isValidExpressionCode('{[1,2,3]}')).toBeFalsy(); + expect(isValidExpressionCode('{tango.stores.app.title}')).toBeFalsy(); }); - test('object2node', () => { + it('object2node', () => { const node = object2node({ url: '/api/backend/clientversion/appmarket/list', method: 'POST', @@ -40,4 +49,22 @@ describe('helpers', () => { } as any); expect(node.type).toEqual('ObjectExpression'); }); + + it('code2expression', () => { + expect(code2expression('')).toBeUndefined(); + expect(code2expression('{tango.stores.app}')).toBeUndefined(); + + expect(code2expression('{ type: window.bar }').type).toEqual('ObjectExpression'); + expect(code2expression('() => {};').type).toEqual('ArrowFunctionExpression'); + expect(code2expression('').type).toBe('JSXElement'); + expect(code2expression('').type).toBe('JSXElement'); + + const arrayCode = ` + [ + { label: 'foo', value: 'foo' }, + { label: 'bar', value: 'bar' }, + ] + `; + expect(code2expression(arrayCode).type).toEqual('ArrayExpression'); + }); }); diff --git a/packages/core/tests/helpers.test.ts b/packages/core/tests/helpers.test.ts index ebadd39..04c936f 100644 --- a/packages/core/tests/helpers.test.ts +++ b/packages/core/tests/helpers.test.ts @@ -25,40 +25,6 @@ describe('helpers', () => { expect(code2ast('function App() {}').type).toEqual('File'); }); - it('code2expression: null', () => { - expect(code2expression('')).toBeUndefined(); - }); - - it('code2expression: object', () => { - const code = ` - { - foo: 'bar', - } - `; - const node = code2expression(code); - expect(node.type).toEqual('ObjectExpression'); - }); - - it('code2expression: arrow function', () => { - expect(code2expression('() => {};').type).toEqual('ArrowFunctionExpression'); - }); - - it('code2expression: list', () => { - const code = ` - [ - { label: 'foo', value: 'foo' }, - { label: 'bar', value: 'bar' }, - ] - `; - const node = code2expression(code); - expect(node.type).toEqual('ArrayExpression'); - }); - - it('code2expression: jsxElement', () => { - const node = code2expression(''); - expect(node.type).toEqual('JSXElement'); - }); - it('parse jsxElement attributes', () => { const node = code2expression( "", @@ -73,11 +39,6 @@ describe('helpers', () => { }); }); - it('code2expression: closed jsxElement', () => { - const node = code2expression(''); - expect(node.type).toEqual('JSXElement'); - }); - it('value2node: number', () => { expect(value2node(1).type).toEqual('NumericLiteral'); }); diff --git a/packages/designer/src/components/variable-tree.tsx b/packages/designer/src/components/variable-tree.tsx index e2965f2..df5b482 100644 --- a/packages/designer/src/components/variable-tree.tsx +++ b/packages/designer/src/components/variable-tree.tsx @@ -372,11 +372,11 @@ export function VariableTree({ const isDeletable = node.showDeleteIcon ?? showDeleteIcon; return ( - + {node.title} {node.type === 'function' && } - + {isDeletable && ( setExpModalVisible(false)} onOk={(nextValue) => { setExpModalVisible(false); - handleChange(wrapCode(nextValue)); + handleChange(`{${nextValue}}`); }} dataSource={actionVariables} /> diff --git a/packages/designer/src/setters/expression-setter.tsx b/packages/designer/src/setters/expression-setter.tsx index dd81498..6daf382 100644 --- a/packages/designer/src/setters/expression-setter.tsx +++ b/packages/designer/src/setters/expression-setter.tsx @@ -1,15 +1,12 @@ import React, { useState, useEffect, useCallback } from 'react'; import { Box, Text, css } from 'coral-system'; import { Modal } from 'antd'; -import { isValidExpressionCode, value2expressionCode } from '@music163/tango-core'; import { - isVariableString, - getVariableContent, - noop, - useBoolean, - getValue, - wrapCode, -} from '@music163/tango-helpers'; + isValidExpressionCode, + isWrappedByExpressionContainer, + value2expressionCode, +} from '@music163/tango-core'; +import { getVariableContent, noop, useBoolean, getValue } from '@music163/tango-helpers'; import { CloseCircleFilled, ExpandAltOutlined } from '@ant-design/icons'; import { IconButton, Panel, InputCode } from '@music163/tango-ui'; import { FormItemComponentProps } from '@music163/tango-setting-form'; @@ -18,7 +15,7 @@ import { EditableVariableTree, IVariableTreeNode } from '../components'; import { useSandboxQuery } from '../context'; export const expressionValueValidate = (value: string) => { - if (isVariableString(value)) { + if (isWrappedByExpressionContainer(value)) { const exp = getVariableContent(value); if (!isValidExpressionCode(exp)) { return '表达式存在语法错误!'; @@ -27,7 +24,7 @@ export const expressionValueValidate = (value: string) => { }; export const jsonValueValidate = (value: string) => { - if (isVariableString(value)) { + if (isWrappedByExpressionContainer(value)) { const jsonStr = getVariableContent(value); try { JSON.parse(jsonStr); @@ -78,16 +75,25 @@ export function ExpressionSetter(props: ExpressionSetterProps) { const change = useCallback( (code: string) => { + if (code === valueProp) { + return; + } + + let ret; + if (!code) { - onChange(undefined); + // do nothing + } else if (isWrappedByExpressionContainer(code)) { + ret = code; + } else { + ret = `{${code}}`; + } + + if (ret === valueProp) { return; } - if (getVariableContent(code) === value2expressionCode(valueProp)) { - return; - } - - onChange(wrapCode(code)); + onChange(ret); }, [valueProp, onChange], ); diff --git a/packages/designer/src/sidebar/datasource-panel/interface-config.tsx b/packages/designer/src/sidebar/datasource-panel/interface-config.tsx index 453a31b..3e84a78 100644 --- a/packages/designer/src/sidebar/datasource-panel/interface-config.tsx +++ b/packages/designer/src/sidebar/datasource-panel/interface-config.tsx @@ -6,13 +6,13 @@ import { PlayCircleOutlined } from '@ant-design/icons'; import { Form, InputCode, Panel, JsonView, Search } from '@music163/tango-ui'; import { isNil, - isVariableString, getVariableContent, isValidFunctionCode, logger, code2object, filterTreeData, } from '@music163/tango-helpers'; +import { isWrappedByExpressionContainer } from '@music163/tango-core'; import { useSandboxQuery } from '../../context'; import { VariableTree } from '../../components'; @@ -187,7 +187,10 @@ const DataSourceView = observer(({ onAdd, onUpdate, onDelete }: DataServiceViewP const shapeValues = { ...val }; delete shapeValues.type; // 兼容旧版,如果 formatter 包裹了 {} 则删掉首尾 - if (shapeValues.formatter && isVariableString(shapeValues.formatter)) { + if ( + shapeValues.formatter && + isWrappedByExpressionContainer(shapeValues.formatter) + ) { shapeValues.formatter = getVariableContent(shapeValues.formatter); } return shapeValues; diff --git a/packages/helpers/src/helpers/string.ts b/packages/helpers/src/helpers/string.ts index e6c3d89..ab9821d 100644 --- a/packages/helpers/src/helpers/string.ts +++ b/packages/helpers/src/helpers/string.ts @@ -182,6 +182,8 @@ const templatePattern = /^{(.+)}$/s; /** * 判断给定字符串是否为变量字符串 + * @deprecated 使用 isWrappedByExpressionContainer 代替 + * * @example {[]} * @example {{}} * @example {this.foo} @@ -191,6 +193,7 @@ const templatePattern = /^{(.+)}$/s; */ export function isVariableString(str: string) { // 先检查是否是简单的对象 + // FIXME: 这里有问题,如果代码中有引用,会被误判 if (code2object(str)) { return false; } @@ -205,6 +208,8 @@ export function isVariableString(str: string) { * @example "hello" => {"hello"} * @example () => {} => {() => {}} * + * @deprecated 有问题,不要使用 + * * @param code 输入代码 * @returns 加上花括号后的代码 */ diff --git a/packages/setting-form/package.json b/packages/setting-form/package.json index 67eded2..af6bf81 100644 --- a/packages/setting-form/package.json +++ b/packages/setting-form/package.json @@ -28,6 +28,7 @@ }, "dependencies": { "@ant-design/icons": "^4.8.0", + "@music163/tango-core": "^0.2.4", "@music163/tango-helpers": "^0.1.6", "@music163/tango-ui": "^0.1.9", "antd": "^4.24.2", diff --git a/packages/setting-form/src/form-item.tsx b/packages/setting-form/src/form-item.tsx index 7a7cc8e..7c10406 100644 --- a/packages/setting-form/src/form-item.tsx +++ b/packages/setting-form/src/form-item.tsx @@ -4,10 +4,10 @@ import { observer } from 'mobx-react-lite'; import { clone, ComponentPropType, - isVariableString, SetterOnChangeDetailType, useBoolean, } from '@music163/tango-helpers'; +import { isWrappedByExpressionContainer } from '@music163/tango-core'; import { IconFont, ToggleButton } from '@music163/tango-ui'; import { QuestionCircleOutlined } from '@ant-design/icons'; import { InputProps, Tooltip } from 'antd'; @@ -83,7 +83,7 @@ export function createFormItem(options: IFormItemCreateOptions) { const value = toJS(field.value ?? defaultValue); const disableVariableSetter = disableSwitchExpressionSetter ?? disableVariableSetterProp; const [isVariable, { toggle: toggleIsVariable }] = useBoolean( - () => !disableVariableSetter && isVariableString(value), + () => !disableVariableSetter && isWrappedByExpressionContainer(value), ); const setterName = isVariable ? 'expressionSetter' : setter;