mirror of
https://github.com/NetEase/tango.git
synced 2026-09-01 15:03:03 +08:00
feat: check file errors & show errors overlay
This commit is contained in:
@@ -3,7 +3,7 @@
|
||||
*/
|
||||
import generator, { GeneratorOptions } from '@babel/generator';
|
||||
import * as t from '@babel/types';
|
||||
import { logger, wrapCode } from '@music163/tango-helpers';
|
||||
import { Dict, logger, wrapCode } from '@music163/tango-helpers';
|
||||
import { formatCode } from '../string';
|
||||
|
||||
const defaultGeneratorOptions: GeneratorOptions = {
|
||||
@@ -25,19 +25,12 @@ export function ast2code(ast: t.Node, options: GeneratorOptions = defaultGenerat
|
||||
return code;
|
||||
}
|
||||
|
||||
const bracketPattern = /^\(.+\)$/s;
|
||||
|
||||
/**
|
||||
* 是否被 () 包裹
|
||||
*
|
||||
* @example ({ foo: 'foo' }) -> true
|
||||
* @example { foo: 'foo' } -> false
|
||||
*
|
||||
* @param str 目标字符串
|
||||
*/
|
||||
function isWrappingWithBrackets(str: string) {
|
||||
return bracketPattern.test(str);
|
||||
}
|
||||
const bracketPattern = /^\(.+\)$/;
|
||||
|
||||
/**
|
||||
* 将表达式生成为块级代码
|
||||
@@ -54,7 +47,7 @@ export function expression2code(node: t.Expression) {
|
||||
|
||||
const isWrappingExpression = t.isObjectExpression(node) || t.isFunctionExpression(node);
|
||||
|
||||
if (isWrappingExpression && isWrappingWithBrackets(ret)) {
|
||||
if (isWrappingExpression && bracketPattern.test(ret)) {
|
||||
// 如果是对象,输出包含 ({}),则去掉首尾的括号
|
||||
ret = ret.slice(1, -1);
|
||||
}
|
||||
@@ -195,7 +188,7 @@ export function node2value(node: t.Node, isWrapCode = true): any {
|
||||
);
|
||||
if (isSimpleObject) {
|
||||
// simple object: { key1, key2, key3 }
|
||||
ret = node.properties.reduce((prev, propertyNode) => {
|
||||
ret = node.properties.reduce<Dict>((prev, propertyNode) => {
|
||||
if (propertyNode.type === 'ObjectProperty') {
|
||||
const key = keyNode2value(propertyNode.key);
|
||||
const value = node2value(propertyNode.value, isWrapCode);
|
||||
|
||||
@@ -68,11 +68,7 @@ export function isValidExpressionCode(code: string) {
|
||||
* @returns
|
||||
*/
|
||||
export function code2ast(code: string): t.File {
|
||||
try {
|
||||
return parse(code, babelParserConfig);
|
||||
} catch (err) {
|
||||
logger.error('[code2ast failed!]', err);
|
||||
}
|
||||
return parse(code, babelParserConfig);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -175,7 +171,7 @@ export function value2node(
|
||||
* 将 js 普通对象解析为 t.Node
|
||||
*/
|
||||
export function object2node(
|
||||
obj: object,
|
||||
obj: Dict<any>,
|
||||
getValueNode: (value: any, key?: string) => t.Expression = value2node,
|
||||
) {
|
||||
if (!isPlainObject(obj)) {
|
||||
|
||||
@@ -1009,7 +1009,7 @@ export function serviceConfig2Node(payload: object) {
|
||||
});
|
||||
}
|
||||
|
||||
export function updateServiceConfigToServiceFile(ast: t.File, config: Dict<object>) {
|
||||
export function updateServiceConfigToServiceFile(ast: t.File, config: Dict<Dict>) {
|
||||
traverse(ast, {
|
||||
CallExpression(path) {
|
||||
const calleeName = keyNode2value(path.node.callee) as string;
|
||||
@@ -1017,7 +1017,7 @@ export function updateServiceConfigToServiceFile(ast: t.File, config: Dict<objec
|
||||
const configNode = path.node.arguments[0];
|
||||
|
||||
if (t.isObjectExpression(configNode)) {
|
||||
const newPropertiesNodeMap = Object.keys(config).reduce((properties, key) => {
|
||||
const newPropertiesNodeMap: Dict = Object.keys(config).reduce<Dict>((properties, key) => {
|
||||
const serviceConfig = config[key];
|
||||
const property = t.objectProperty(t.identifier(key), serviceConfig2Node(serviceConfig));
|
||||
properties[key] = property;
|
||||
|
||||
@@ -24,6 +24,16 @@ export class TangoFile {
|
||||
*/
|
||||
lastModified: number;
|
||||
|
||||
/**
|
||||
* 文件是否存在错误
|
||||
*/
|
||||
isError: boolean;
|
||||
|
||||
/**
|
||||
* 文件的错误消息
|
||||
*/
|
||||
errorMessage: string;
|
||||
|
||||
_code: string;
|
||||
_cleanCode: string;
|
||||
|
||||
@@ -40,6 +50,7 @@ export class TangoFile {
|
||||
this.filename = props.filename;
|
||||
this.type = props.type;
|
||||
this.lastModified = Date.now();
|
||||
this.isError = false;
|
||||
|
||||
// 这里主要是为了解决 umi ts 编译错误的问题,@see https://github.com/umijs/umi/issues/7594
|
||||
if (isSyncCode) {
|
||||
|
||||
@@ -319,6 +319,10 @@ export interface IWorkspace {
|
||||
get bizComps(): string[];
|
||||
get baseComps(): string[];
|
||||
get localComps(): string[];
|
||||
/**
|
||||
* 文件错误列表
|
||||
*/
|
||||
get fileErrors(): string[];
|
||||
/**
|
||||
* 是否是合法的项目
|
||||
*/
|
||||
|
||||
@@ -38,18 +38,26 @@ export class TangoModule extends TangoFile {
|
||||
*/
|
||||
update(code?: string, isFormatCode = true, refreshWorkspace = true) {
|
||||
this.lastModified = Date.now();
|
||||
if (isNil(code)) {
|
||||
this._syncByAst();
|
||||
} else {
|
||||
this._syncByCode(code, isFormatCode);
|
||||
}
|
||||
|
||||
this._analysisAst();
|
||||
try {
|
||||
if (isNil(code)) {
|
||||
this._syncByAst();
|
||||
} else {
|
||||
this._syncByCode(code, isFormatCode);
|
||||
}
|
||||
this._analysisAst();
|
||||
|
||||
this.workspace.onFilesChange([this.filename]);
|
||||
this.isError = false;
|
||||
this.errorMessage = undefined;
|
||||
|
||||
if (refreshWorkspace) {
|
||||
this.workspace.refresh([this.filename]);
|
||||
this.workspace.onFilesChange([this.filename]);
|
||||
|
||||
if (refreshWorkspace) {
|
||||
this.workspace.refresh([this.filename]);
|
||||
}
|
||||
} catch (err: any) {
|
||||
this.isError = true;
|
||||
this.errorMessage = err.message;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -110,6 +118,8 @@ export class TangoJsModule extends TangoModule {
|
||||
makeObservable(this, {
|
||||
_code: observable,
|
||||
_cleanCode: observable,
|
||||
isError: observable,
|
||||
errorMessage: observable,
|
||||
code: computed,
|
||||
cleanCode: computed,
|
||||
update: action,
|
||||
|
||||
@@ -28,6 +28,8 @@ export class TangoRouteModule extends TangoModule {
|
||||
_routes: observable,
|
||||
_code: observable,
|
||||
_cleanCode: observable,
|
||||
isError: observable,
|
||||
errorMessage: observable,
|
||||
routes: computed,
|
||||
code: computed,
|
||||
cleanCode: computed,
|
||||
|
||||
@@ -49,6 +49,8 @@ export class TangoServiceModule extends TangoModule {
|
||||
_baseConfig: observable,
|
||||
_code: observable,
|
||||
_cleanCode: observable,
|
||||
isError: observable,
|
||||
errorMessage: observable,
|
||||
serviceFunctions: computed,
|
||||
baseConfig: computed,
|
||||
cleanCode: computed,
|
||||
|
||||
@@ -31,6 +31,8 @@ export class TangoStoreEntryModule extends TangoModule {
|
||||
_stores: observable,
|
||||
_code: observable,
|
||||
_cleanCode: observable,
|
||||
isError: observable,
|
||||
errorMessage: observable,
|
||||
stores: computed,
|
||||
code: computed,
|
||||
cleanCode: computed,
|
||||
|
||||
@@ -138,6 +138,9 @@ export class TangoViewModule extends TangoModule implements IViewFile {
|
||||
_code: observable,
|
||||
_cleanCode: observable,
|
||||
|
||||
isError: observable,
|
||||
errorMessage: observable,
|
||||
|
||||
code: computed,
|
||||
cleanCode: computed,
|
||||
|
||||
@@ -320,7 +323,7 @@ export class TangoViewModule extends TangoModule implements IViewFile {
|
||||
updateNodeAttributes(nodeId: string, config: Record<string, any>, relatedImports?: string[]) {
|
||||
if (relatedImports && relatedImports.length) {
|
||||
// 导入依赖的组件
|
||||
const newImportData = relatedImports.reduce((prev, name) => {
|
||||
const newImportData = relatedImports.reduce<Dict<IImportSpecifierData[]>>((prev, name) => {
|
||||
const proto = this.workspace.getPrototype(name);
|
||||
const { source, specifiers } = prototype2importDeclarationData(proto, this.filename);
|
||||
const existSpecifiers: IImportSpecifierData[] = prev[source];
|
||||
|
||||
@@ -237,6 +237,16 @@ export class Workspace extends EventTarget implements IWorkspace {
|
||||
return Object.keys(this.componentsEntryModule?.exportList || {});
|
||||
}
|
||||
|
||||
get fileErrors() {
|
||||
const list: string[] = [];
|
||||
this.files.forEach((file) => {
|
||||
if (file.isError) {
|
||||
list.push(file.errorMessage);
|
||||
}
|
||||
});
|
||||
return list;
|
||||
}
|
||||
|
||||
constructor(options?: IWorkspaceOptions) {
|
||||
super();
|
||||
this.history = new TangoHistory(this);
|
||||
@@ -274,6 +284,7 @@ export class Workspace extends EventTarget implements IWorkspace {
|
||||
activeViewFile: observable,
|
||||
pages: computed,
|
||||
bizComps: computed,
|
||||
fileErrors: computed,
|
||||
setActiveRoute: action,
|
||||
addFile: action,
|
||||
removeFile: action,
|
||||
@@ -300,6 +311,12 @@ export class Workspace extends EventTarget implements IWorkspace {
|
||||
}
|
||||
} else {
|
||||
// 从设计切换到源码模式
|
||||
|
||||
if (this.fileErrors.length) {
|
||||
// 工作区文件存在语法错误,不同步
|
||||
return;
|
||||
}
|
||||
|
||||
this.editorState.clear();
|
||||
this.editorState.addFiles(this.listFileData());
|
||||
}
|
||||
|
||||
@@ -71,7 +71,7 @@ describe('helpers', () => {
|
||||
});
|
||||
|
||||
it('expression2code: arrowFunctionExpression', () => {
|
||||
expect(expression2code(code2expression('() => {}'))).toEqual('() => {}');
|
||||
expect(expression2code(code2expression('() => {\n\n}\n'))).toEqual('() => {\n\n}');
|
||||
});
|
||||
|
||||
it('expression2code: memberExpression', () => {
|
||||
|
||||
@@ -0,0 +1,65 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { Box, css } from 'coral-system';
|
||||
import { observer, useWorkspace } from '@music163/tango-context';
|
||||
import { CloseOutlined } from '@ant-design/icons';
|
||||
|
||||
const errorMessageStyle = css`
|
||||
padding: 12px;
|
||||
white-space: pre-wrap;
|
||||
`;
|
||||
|
||||
/**
|
||||
* 文件错误提示浮层
|
||||
*/
|
||||
export const FilesErrorOverlay = observer(() => {
|
||||
const [isVisible, setIsVisible] = useState<boolean>(true);
|
||||
const [errors, setErrors] = useState<string[]>([]);
|
||||
const workspace = useWorkspace();
|
||||
|
||||
useEffect(() => {
|
||||
setErrors(workspace.fileErrors);
|
||||
}, [workspace.fileErrors]);
|
||||
|
||||
const handleClose = () => {
|
||||
setIsVisible(false);
|
||||
};
|
||||
|
||||
if (workspace.fileErrors.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!isVisible) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Box
|
||||
className="WorkspaceErrorOverlay"
|
||||
position="absolute"
|
||||
top="0"
|
||||
left="0"
|
||||
right="0"
|
||||
height="100%"
|
||||
bg="rgba(244, 244, 244, 0.9)"
|
||||
color="red"
|
||||
zIndex={1999}
|
||||
>
|
||||
<Box position="relative" p="4">
|
||||
<CloseOutlined
|
||||
style={{
|
||||
position: 'absolute',
|
||||
top: 12,
|
||||
right: 12,
|
||||
color: '#000',
|
||||
}}
|
||||
onClick={handleClose}
|
||||
/>
|
||||
<Box css={errorMessageStyle}>
|
||||
{errors.map((error, index) => (
|
||||
<Box key={index}>{error}</Box>
|
||||
))}
|
||||
</Box>
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
});
|
||||
@@ -4,3 +4,4 @@ export * from './input-kv';
|
||||
export * from './variable-tree';
|
||||
export * from './variable-tree-modal';
|
||||
export * from './components-popover';
|
||||
export * from './files-error-overlay';
|
||||
|
||||
@@ -3,7 +3,7 @@ import cx from 'classnames';
|
||||
import { Box, HTMLCoralProps } from 'coral-system';
|
||||
import { observer, useDesigner } from '@music163/tango-context';
|
||||
import { DesignerViewType } from '@music163/tango-core';
|
||||
import { ComponentsPopover } from './components';
|
||||
import { ComponentsPopover, FilesErrorOverlay } from './components';
|
||||
|
||||
export interface WorkspaceViewProps extends HTMLCoralProps<'div'> {
|
||||
/**
|
||||
@@ -35,6 +35,7 @@ export const WorkspaceView = observer((props: WorkspaceViewProps) => {
|
||||
{children}
|
||||
{/* 添加组件弹层 */}
|
||||
{display === 'block' && <ComponentsPopover type="inner" isControlled />}
|
||||
{mode === 'design' && <FilesErrorOverlay />}
|
||||
</Box>
|
||||
);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user