feat!: refactor base class (#191)

* feat: refactor types

* feat: rename models

* fix: update types

* fix: init workspace and viewNode

* fix: update fileTypes

* fix: update

* fix: update
This commit is contained in:
Wells
2024-09-10 16:02:53 +08:00
committed by GitHub
parent 1802fc0eb8
commit eae45003c6
38 changed files with 1895 additions and 2041 deletions
+6 -2
View File
@@ -1,4 +1,4 @@
import type { Engine } from '@music163/tango-core';
import type { AbstractCodeWorkspace, Engine } from '@music163/tango-core';
import { IVariableTreeNode, createContext } from '@music163/tango-helpers';
export interface ITangoEngineContext {
@@ -21,8 +21,12 @@ const [TangoEngineProvider, useTangoEngine] = createContext<ITangoEngineContext>
export { TangoEngineProvider };
/**
* 获取 CodeWorkspace 实例
* @returns
*/
export const useWorkspace = () => {
return useTangoEngine()?.engine.workspace;
return useTangoEngine()?.engine.workspace as AbstractCodeWorkspace;
};
export const useDesigner = () => {
+2 -2
View File
@@ -1,12 +1,12 @@
import { MenuDataType } from '@music163/tango-helpers';
import { Designer, DesignerViewType, Engine, SimulatorNameType } from './models';
import { IWorkspace } from './models/interfaces';
import { AbstractWorkspace } from './models/abstract-workspace';
interface ICreateEngineOptions {
/**
* 自定义工作区
*/
workspace?: IWorkspace;
workspace?: AbstractWorkspace;
/**
* 菜单信息
*/
+2 -2
View File
@@ -28,7 +28,7 @@ import { isDefineService, isDefineStore, isTangoVariable } from '../assert';
import type {
IRouteData,
IStorePropertyData,
ITangoViewNodeData,
IViewNodeData,
IImportDeclarationPayload,
InsertChildPositionType,
IImportSpecifierData,
@@ -1234,7 +1234,7 @@ export function cloneJSXElement(node: t.JSXElement, overrideProps?: Dict) {
export function traverseViewFile(ast: t.File, idGenerator: IdGenerator) {
const imports: Record<string, IImportSpecifierData[]> = {};
const importedModules: Dict<IImportDeclarationPayload | IImportDeclarationPayload[]> = {};
const nodes: Array<ITangoViewNodeData<t.JSXElement>> = [];
const nodes: Array<IViewNodeData<t.JSXElement>> = [];
const cloneAst = t.cloneNode(ast, true, true);
const cleanAst = clearTrackingData(cloneAst);
const variables: string[] = []; // 使用的 tango 变量
+13 -21
View File
@@ -8,65 +8,57 @@ import { FileType } from './../types';
export function inferFileType(filename: string): FileType {
// 增加 tangoConfigJson Module
if (/\/tango\.config\.json$/.test(filename)) {
return FileType.TangoConfigJson;
return FileType.TangoConfigJsonFile;
}
if (/\/appJson\.json$/.test(filename)) {
return FileType.AppJson;
return FileType.AppJsonFile;
}
if (/\/package\.json$/.test(filename)) {
return FileType.PackageJson;
return FileType.PackageJsonFile;
}
if (/\/routes\.js$/.test(filename)) {
return FileType.RouteModule;
return FileType.JsRouteConfigFile;
}
// 所有 pages 下的 js 文件均认为是有效的 viewModule
if (/\/pages\/.+\.jsx?$/.test(filename)) {
return FileType.JsxViewModule;
return FileType.JsViewFile;
}
// 所有 pages 下的 js 文件均认为是有效的 viewModule
if (/\/pages\/.+\.schema\.json?$/.test(filename)) {
return FileType.JsonViewModule;
return FileType.JsonViewFile;
}
if (/\/(blocks|components)\/index\.js/.test(filename)) {
return FileType.ComponentsEntryModule;
return FileType.JsLocalComponentsEntryFile;
}
if (/\/services\/.+\.js$/.test(filename)) {
return FileType.ServiceModule;
return FileType.JsServiceFile;
}
if (/service\.js$/.test(filename)) {
return FileType.ServiceModule;
return FileType.JsServiceFile;
}
if (/\/stores\/index\.js$/.test(filename)) {
return FileType.StoreEntryModule;
return FileType.JsStoreEntryFile;
}
if (/\/stores\/.+\.js$/.test(filename)) {
return FileType.StoreModule;
return FileType.JsStoreFile;
}
if (/\.jsx?$/.test(filename)) {
return FileType.Module;
return FileType.JsFile;
}
if (/\.json$/.test(filename)) {
return FileType.Json;
}
if (/\.less$/.test(filename)) {
return FileType.Less;
}
if (/\.scss$/.test(filename)) {
return FileType.Scss;
return FileType.JsonFile;
}
return FileType.File;
@@ -0,0 +1,264 @@
import {
Dict,
isStoreVariablePath,
parseServiceVariablePath,
parseStoreVariablePath,
} from '@music163/tango-helpers';
import { inferFileType, getFilepath } from '../helpers';
import { TangoFile } from './file';
import { FileType } from '../types';
import { JsRouteConfigFile } from './js-route-config-file';
import { JsStoreEntryFile } from './js-store-entry-file';
import { JsServiceFile } from './js-service-file';
import { JsViewFile } from './js-view-file';
import { JsLocalComponentsEntryFile } from './js-local-components-entry-file';
import { JsAppEntryFile } from './js-app-entry-file';
import { JsFile } from './js-file';
import { AbstractWorkspace, IWorkspaceInitConfig } from './abstract-workspace';
import { JsonFile } from './json-file';
import { JsStoreFile } from './js-store-file';
/**
* CodeWorkspace 抽象基类
*/
export abstract class AbstractCodeWorkspace extends AbstractWorkspace {
/**
* 模型入口配置模块
*/
storeEntryModule: JsStoreEntryFile;
/**
* 状态管理模块
*/
storeModules: Record<string, JsStoreFile>;
/**
* 数据服务模块
*/
serviceModules: Record<string, JsServiceFile>;
constructor(options: IWorkspaceInitConfig) {
super(options);
this.storeModules = {};
this.serviceModules = {};
if (options?.files) {
this.addFiles(options.files);
}
}
/**
* 添加文件到工作区
* @param filename 文件名
* @param code 代码片段
* @param fileType 模块类型
*/
addFile(filename: string, code: string, fileType?: FileType) {
if (!fileType && filename === this.entry) {
fileType = FileType.JsAppEntryFile;
}
const moduleType = fileType || inferFileType(filename);
const props = {
filename,
code,
type: moduleType,
};
let module;
switch (moduleType) {
case FileType.JsAppEntryFile:
module = new JsAppEntryFile(this, props);
this.jsAppEntryFile = module;
break;
case FileType.JsStoreEntryFile:
module = new JsStoreEntryFile(this, props);
this.storeEntryModule = module;
break;
case FileType.JsLocalComponentsEntryFile:
module = new JsLocalComponentsEntryFile(this, props);
this.componentsEntryModule = module;
break;
case FileType.JsRouteConfigFile: {
module = new JsRouteConfigFile(this, props);
this.routeModule = module;
// check if activeRoute exists
const route = module.routes.find((item) => item.path === this.activeRoute);
if (!route) {
this.setActiveRoute(module.routes[0]?.path);
}
break;
}
case FileType.JsViewFile:
module = new JsViewFile(this, props);
break;
case FileType.JsServiceFile:
module = new JsServiceFile(this, props);
this.serviceModules[module.name] = module;
break;
case FileType.JsStoreFile:
module = new JsStoreFile(this, props);
this.storeModules[module.name] = module;
break;
case FileType.JsFile:
module = new JsFile(this, props);
break;
case FileType.PackageJsonFile:
module = new JsonFile(this, props);
this.packageJson = module;
break;
case FileType.TangoConfigJsonFile:
module = new JsonFile(this, props);
this.tangoConfigJson = module;
break;
case FileType.JsonFile:
module = new JsonFile(this, props);
break;
default:
module = new TangoFile(this, props);
}
this.files.set(filename, module);
}
addServiceFile(serviceName: string, code: string) {
const filename = `/src/services/${serviceName}.js`;
this.addFile(filename, code, FileType.JsServiceFile);
const indexServiceModule = this.serviceModules.index;
indexServiceModule?.addImportDeclaration(`./${serviceName}`, []).update();
}
addStoreFile(storeName: string, code: string) {
const filename = `/src/stores/${storeName}.js`;
this.addFile(filename, code);
if (!this.storeEntryModule) {
this.addFile('/src/stores/index.js', '');
}
this.storeEntryModule.addStore(storeName).update();
}
/**
* 添加新的模型文件
* @deprecated 使用 addStoreFile 代替
*/
addStoreModule(name: string, code: string) {
this.addStoreFile(name, code);
}
/**
* 删除模型文件
* @param name
*/
removeStoreModule(name: string) {
const filename = getFilepath(name, '/src/stores', '.js');
this.storeEntryModule.removeStore(name).update();
this.removeFile(filename);
}
/**
* 添加模型属性
* @param storeName
* @param stateName
* @param initValue
*/
addStoreState(storeName: string, stateName: string, initValue: string) {
this.storeModules[storeName]?.addState(stateName, initValue).update();
}
/**
* 删除模型属性
* @param storeName
* @param stateName
*/
removeStoreState(storeName: string, stateName: string) {
this.storeModules[storeName]?.removeState(stateName).update();
}
/**
* 根据变量路径删除状态变量
* @param variablePath
*/
removeStoreVariable(variablePath: string) {
const { storeName, variableName } = parseStoreVariablePath(variablePath);
this.removeStoreState(storeName, variableName);
}
/**
* 根据变量路径更新状态变量的值
* @param variablePath 变量路径
* @param code 变量代码
*/
updateStoreVariable(variablePath: string, code: string) {
if (isStoreVariablePath(variablePath)) {
const { storeName, variableName } = parseStoreVariablePath(variablePath);
this.storeModules[storeName]?.updateState(variableName, code).update();
}
}
/**
* 获取服务函数的详情
* TODO: 不要 services 前缀
* @param serviceKey `services.list` 或 `services.sub.list`
* @returns
*/
getServiceFunction(serviceKey: string) {
const { name, moduleName } = parseServiceVariablePath(serviceKey);
if (!name) {
return;
}
return {
name,
moduleName,
config: this.serviceModules[moduleName]?.serviceFunctions[name],
};
}
/**
* 获取服务函数的列表
* @returns 返回服务函数的列表 { [serviceKey: string]: Dict }
*/
listServiceFunctions() {
const ret: Record<string, Dict> = {};
Object.keys(this.serviceModules).forEach((moduleName) => {
const module = this.serviceModules[moduleName];
Object.keys(module.serviceFunctions).forEach((name) => {
const serviceKey = moduleName === 'index' ? name : [moduleName, name].join('.');
ret[serviceKey] = module.serviceFunctions[name];
});
});
return ret;
}
/**
* 更新服务函数
*/
updateServiceFunction(serviceName: string, payload: Dict, moduleName = 'index') {
this.serviceModules[moduleName].updateServiceFunction(serviceName, payload).update();
}
/**
* 新增服务函数,支持批量添加
*/
addServiceFunction(name: string, config: Dict, moduleName = 'index') {
this.serviceModules[moduleName]?.addServiceFunction(name, config).update();
}
addServiceFunctions(configs: Dict<Dict>, modName = 'index') {
this.serviceModules[modName]?.addServiceFunctions(configs).update();
}
/**
* 删除服务函数
* @param name
*/
removeServiceFunction(serviceKey: string) {
const { moduleName, name } = parseServiceVariablePath(serviceKey);
this.serviceModules[moduleName]?.deleteServiceFunction(name).update();
}
/**
* 更新服务的基础配置
*/
updateServiceBaseConfig(config: Dict, moduleName = 'index') {
this.serviceModules[moduleName]?.updateBaseConfig(config).update();
}
}
+63
View File
@@ -0,0 +1,63 @@
import { AbstractWorkspace } from './abstract-workspace';
import type { FileType, IFileConfig } from '../types';
/**
* 普通文件抽象基类,不进行 AST 解析
*/
export abstract class AbstractFile {
readonly workspace: AbstractWorkspace;
/**
* 文件名
*/
readonly filename: string;
/**
* 文件类型
*/
readonly type: FileType;
/**
* 最近修改的时间戳
*/
lastModified: number;
/**
* 文件解析是否出错
*/
isError: boolean;
/**
* 文件解析错误消息
*/
errorMessage: string;
_code: string;
_cleanCode: string;
get code() {
return this._code;
}
// FIXME: cleanCode 是不是只有 viewFile 有 ????
get cleanCode() {
return this._cleanCode;
}
constructor(workspace: AbstractWorkspace, props: IFileConfig, isSyncCode = true) {
this.workspace = workspace;
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) {
this.update(props.code);
}
}
/**
* 更新文件内容
*/
abstract update(code?: string): void;
}
@@ -1,5 +1,4 @@
import * as t from '@babel/types';
import { action, computed, makeObservable, observable } from 'mobx';
import { isNil } from '@music163/tango-helpers';
import {
code2ast,
@@ -9,16 +8,16 @@ import {
addImportDeclaration,
updateImportDeclaration,
} from '../helpers';
import { TangoFile } from './file';
import { IFileConfig, IImportSpecifierData, ImportDeclarationDataType } from '../types';
import { IWorkspace } from './interfaces';
import { AbstractWorkspace } from './abstract-workspace';
import { AbstractFile } from './abstract-file';
/**
* JS
* JS
* - ast this
* - observable state _foo getter
*/
export class TangoModule extends TangoFile {
export abstract class AbstractJsFile extends AbstractFile {
ast: t.File;
/**
@@ -31,7 +30,7 @@ export class TangoModule extends TangoFile {
*/
importList: ImportDeclarationDataType;
constructor(workspace: IWorkspace, props: IFileConfig, isSyncCode = true) {
constructor(workspace: AbstractWorkspace, props: IFileConfig, isSyncCode = true) {
super(workspace, props, isSyncCode);
}
@@ -137,24 +136,3 @@ export class TangoModule extends TangoFile {
this.importList = imports;
}
}
/**
* JS
*/
export class TangoJsModule extends TangoModule {
constructor(workspace: IWorkspace, props: IFileConfig) {
super(workspace, props, false);
this.update(props.code, true, false);
makeObservable(this, {
_code: observable,
_cleanCode: observable,
isError: observable,
errorMessage: observable,
code: computed,
cleanCode: computed,
update: action,
updateAst: action,
});
}
}
@@ -0,0 +1,106 @@
import { getValue, isNil, logger, setValue } from '@music163/tango-helpers';
import type { IFileConfig } from '../types';
import { formatCode } from '../helpers';
import { AbstractWorkspace } from './abstract-workspace';
import { AbstractFile } from './abstract-file';
export abstract class AbstractJsonFile extends AbstractFile {
_object;
abstract get json(): object;
constructor(workspace: AbstractWorkspace, props: IFileConfig) {
super(workspace, props, false);
this._object = {};
this.update(props.code);
}
update(code?: string) {
this.lastModified = Date.now();
if (isNil(code)) {
// 基于最新的 json 同步代码
let newCode = JSON.stringify(this._object);
try {
newCode = formatCode(newCode, 'json');
} catch (err) {
logger.error(err);
return;
}
this._code = newCode;
this._cleanCode = newCode;
} else {
try {
// 基于传入的代码,同步 json 对象
code = formatCode(code, 'json');
} catch (err) {
logger.error(err);
return;
}
this._code = code;
this._cleanCode = code;
try {
const json = JSON.parse(code);
this._object = json;
} catch (err) {
logger.error(err);
}
}
this.workspace.onFilesChange([this.filename]);
}
/**
* 根据路径取值
* @param valuePath
* @returns
*/
getValue(valuePath: string) {
return getValue(this.json, valuePath);
}
/**
* 根据路径设置值
* @param valuePath
* @param visitor
*/
setValue(valuePath: string, visitor: (targetValue: any) => any) {
const target = this.getValue(valuePath);
let next: unknown;
if (typeof visitor === 'function') {
next = visitor?.(target);
} else {
next = visitor;
}
if (next !== undefined) {
setValue(this._object, valuePath, next);
}
return this;
}
/**
* 根据路径删除值
* @param valuePath
* @param visitor
*/
deleteValue(valuePath: string) {
const pathList = valuePath.split('.');
const lastPath = pathList.pop();
const parentPath = pathList.join('.');
let target;
if (parentPath) {
target = this.getValue(parentPath);
} else {
target = this.json;
}
if (!target) {
return this;
}
delete target[lastPath];
if (parentPath) {
this.setValue(parentPath, target);
} else {
this._object = target;
}
return this;
}
}
@@ -0,0 +1,59 @@
import { Dict } from '@music163/tango-helpers';
import { AbstractFile } from './abstract-file';
export interface IViewNodeInitConfig<RawNodeType = unknown, ViewFileType = AbstractFile> {
id: string;
component: string;
rawNode: RawNodeType;
file: ViewFileType;
}
export abstract class AbstractViewNode<RawNodeType = unknown, ViewFileType = AbstractFile> {
/**
* 节点 ID
*/
readonly id: string;
/**
* 节点对应的组件名
*/
readonly component: string;
readonly rawNode: RawNodeType;
/**
* 节点所属的文件对象
*/
file: ViewFileType;
/**
* 节点所属的文件对象
*/
props: Record<string, any>;
/**
* 节点的位置信息
*/
abstract get loc(): unknown;
constructor(props: IViewNodeInitConfig<RawNodeType, ViewFileType>) {
this.id = props.id;
this.component = props.component;
this.rawNode = props.rawNode;
this.file = props.file;
}
/**
* 销毁当前节点,清空文件和节点的关联关系
*/
destroy() {
this.file = undefined;
}
/**
* 返回克隆后的 ast 节点
* @param overrideProps 额外设置给克隆节点的属性
* @returns 返回克隆的原始节点
*/
abstract cloneRawNode(overrideProps?: Dict): RawNodeType;
}
File diff suppressed because it is too large Load Diff
+3 -3
View File
@@ -1,6 +1,6 @@
import { action, computed, makeObservable, observable, toJS } from 'mobx';
import { IWorkspace } from './interfaces';
import { MenuDataType } from '@music163/tango-helpers';
import { AbstractWorkspace } from './abstract-workspace';
export type SimulatorNameType = 'desktop' | 'phone';
@@ -18,7 +18,7 @@ interface IViewportBounding {
}
interface IDesignerOptions {
workspace: IWorkspace;
workspace: AbstractWorkspace;
simulator?: SimulatorNameType | ISimulatorType;
/**
* 菜单配置
@@ -108,7 +108,7 @@ export class Designer {
*/
_menuData?: MenuDataType = null;
private readonly workspace: IWorkspace;
private readonly workspace: AbstractWorkspace;
get simulator(): ISimulatorType {
return toJS(this._simulator);
+3 -3
View File
@@ -1,7 +1,7 @@
import { action, computed, makeObservable, observable } from 'mobx';
import { ISelectedItemData } from '@music163/tango-helpers';
import { DropTarget } from './drop-target';
import { IWorkspace } from './interfaces';
import { AbstractWorkspace } from './abstract-workspace';
/**
* 拖拽来源类,被拖拽的物体
@@ -22,7 +22,7 @@ export class DragSource {
*/
dropTarget: DropTarget;
private readonly workspace: IWorkspace;
private readonly workspace: AbstractWorkspace;
get node() {
return this.workspace.getNode(this.data?.id, this.data?.filename);
@@ -47,7 +47,7 @@ export class DragSource {
return this.data?.bounding;
}
constructor(workspace: IWorkspace) {
constructor(workspace: AbstractWorkspace) {
this.workspace = workspace;
this.data = null;
this.isDragging = false;
+3 -3
View File
@@ -1,6 +1,6 @@
import { action, computed, makeObservable, observable } from 'mobx';
import { ISelectedItemData } from '@music163/tango-helpers';
import { IWorkspace } from './interfaces';
import { AbstractWorkspace } from './abstract-workspace';
export enum DropMethod {
ReplaceNode = 'replaceNode', // 替换节点
@@ -23,7 +23,7 @@ export class DropTarget {
*/
data: ISelectedItemData;
private readonly workspace: IWorkspace;
private readonly workspace: AbstractWorkspace;
get node() {
return this.workspace.getNode(this.data.id, this.data.filename);
@@ -48,7 +48,7 @@ export class DropTarget {
return this.data?.display;
}
constructor(workspace: IWorkspace) {
constructor(workspace: AbstractWorkspace) {
this.workspace = workspace;
this.method = DropMethod.InsertAfter;
this.data = null;
+2 -2
View File
@@ -1,5 +1,5 @@
import { AbstractWorkspace } from './abstract-workspace';
import { Designer } from './designer';
import { IWorkspace } from './interfaces';
/**
* 设计器引擎
@@ -8,7 +8,7 @@ export class Engine {
/**
* 工作区状态
*/
workspace: IWorkspace;
workspace: AbstractWorkspace;
/**
* 设计器状态
*/
+16 -194
View File
@@ -1,66 +1,22 @@
import { action, computed, makeObservable, observable, toJS } from 'mobx';
import { getValue, isNil, logger, setValue } from '@music163/tango-helpers';
import type { FileType, IFileConfig } from '../types';
import { IWorkspace } from './interfaces';
import { formatCode } from '../helpers';
import { action, computed, makeObservable, observable } from 'mobx';
import { isNil } from '@music163/tango-helpers';
import type { IFileConfig } from '../types';
import { AbstractWorkspace } from './abstract-workspace';
import { AbstractFile } from './abstract-file';
/**
* 普通文件,不进行 AST 解析
*/
export class TangoFile {
readonly workspace: IWorkspace;
/**
* 文件名
*/
readonly filename: string;
/**
* 文件类型
*/
readonly type: FileType;
/**
* 最近修改的时间戳
*/
lastModified: number;
/**
* 文件解析是否出错
*/
isError: boolean;
/**
* 文件解析错误消息
*/
errorMessage: string;
_code: string;
_cleanCode: string;
get code() {
return this._code;
export class TangoFile extends AbstractFile {
constructor(workspace: AbstractWorkspace, props: IFileConfig) {
super(workspace, props, false);
this.update(props.code);
makeObservable(this, {
_code: observable,
_cleanCode: observable,
code: computed,
cleanCode: computed,
update: action,
});
}
get cleanCode() {
return this._cleanCode;
}
constructor(workspace: IWorkspace, props: IFileConfig, isSyncCode = true) {
this.workspace = workspace;
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) {
this.update(props.code);
}
}
/**
* 更新文件内容
*/
update(code?: string) {
if (!isNil(code)) {
this.lastModified = Date.now();
@@ -70,137 +26,3 @@ export class TangoFile {
this.workspace.onFilesChange([this.filename]);
}
}
export class TangoLessFile extends TangoFile {
constructor(workspace: IWorkspace, props: IFileConfig) {
super(workspace, props, false);
this.update(props.code);
makeObservable(this, {
_code: observable,
_cleanCode: observable,
code: computed,
cleanCode: computed,
update: action,
});
}
}
export class TangoJsonFile extends TangoFile {
_object = {};
/**
* @deprecated 使用 file.json 代替
*/
get object() {
return toJS(this._object);
}
get json() {
return toJS(this._object);
}
constructor(workspace: IWorkspace, props: IFileConfig) {
super(workspace, props, false);
this.update(props.code);
makeObservable(this, {
_code: observable,
_cleanCode: observable,
_object: observable,
code: computed,
cleanCode: computed,
object: computed,
json: computed,
update: action,
setValue: action,
});
}
update(code?: string) {
this.lastModified = Date.now();
if (isNil(code)) {
// 基于最新的 json 同步代码
let newCode = JSON.stringify(this._object);
try {
newCode = formatCode(newCode, 'json');
} catch (err) {
logger.error(err);
return;
}
this._code = newCode;
this._cleanCode = newCode;
} else {
try {
// 基于传入的代码,同步 json 对象
code = formatCode(code, 'json');
} catch (err) {
logger.error(err);
return;
}
this._code = code;
this._cleanCode = code;
try {
const json = JSON.parse(code);
this._object = json;
} catch (err) {
logger.error(err);
}
}
this.workspace.onFilesChange([this.filename]);
}
/**
* 根据路径取值
* @param valuePath
* @returns
*/
getValue(valuePath: string) {
return getValue(this.json, valuePath);
}
/**
* 根据路径设置值
* @param valuePath
* @param visitor
*/
setValue(valuePath: string, visitor: (targetValue: any) => any) {
const target = this.getValue(valuePath);
let next: unknown;
if (typeof visitor === 'function') {
next = visitor?.(target);
} else {
next = visitor;
}
if (next !== undefined) {
setValue(this._object, valuePath, next);
}
return this;
}
/**
* 根据路径删除值
* @param valuePath
* @param visitor
*/
deleteValue(valuePath: string) {
const pathList = valuePath.split('.');
const lastPath = pathList.pop();
const parentPath = pathList.join('.');
let target;
if (parentPath) {
target = this.getValue(parentPath);
} else {
target = this.json;
}
if (!target) {
return this;
}
delete target[lastPath];
if (parentPath) {
this.setValue(parentPath, target);
} else {
this._object = target;
}
return this;
}
}
+3 -3
View File
@@ -1,5 +1,5 @@
import { action, computed, makeObservable, observable, toJS } from 'mobx';
import { IWorkspace } from './interfaces';
import { AbstractWorkspace } from './abstract-workspace';
export enum HistoryMessage {
InitView = 'initView',
@@ -43,7 +43,7 @@ export class TangoHistory {
// 最多记录数
_maxSize = 100;
private readonly workspace: IWorkspace;
private readonly workspace: AbstractWorkspace;
get index() {
return this._index;
@@ -65,7 +65,7 @@ export class TangoHistory {
return this._records.length > this._index + 1;
}
constructor(workspace: IWorkspace) {
constructor(workspace: AbstractWorkspace) {
this.workspace = workspace;
makeObservable(this, {
+21 -13
View File
@@ -1,17 +1,25 @@
export * from './engine';
export * from './workspace';
export * from './abstract-workspace';
export * from './abstract-code-workspace';
export * from './abstract-file';
export * from './abstract-js-file';
export * from './abstract-json-file';
export * from './abstract-view-node';
export * from './designer';
export * from './drop-target';
export * from './select-source';
export * from './drag-source';
export * from './history';
export * from './drop-target';
export * from './engine';
export * from './file';
export * from './module';
export * from './entry-module';
export * from './route-module';
export * from './service-module';
export * from './store-module';
export * from './view-module';
export * from './component-module';
export * from './node';
export * from './history';
export * from './interfaces';
export * from './js-app-entry-file';
export * from './js-file';
export * from './js-local-components-entry-file';
export * from './js-route-config-file';
export * from './js-service-file';
export * from './js-store-entry-file';
export * from './js-store-file';
export * from './js-view-file';
export * from './json-file';
export * from './select-source';
export * from './view-node';
export * from './workspace';
+20 -260
View File
@@ -1,28 +1,17 @@
import { IComponentPrototype, Dict, ITangoConfigJson } from '@music163/tango-helpers';
import { TangoHistory } from './history';
import { SelectSource } from './select-source';
import { DragSource } from './drag-source';
import { Dict } from '@music163/tango-helpers';
import {
IFileConfig,
FileType,
InsertChildPositionType,
ITangoConfigPackages,
IPageConfigData,
IImportSpecifierSourceData,
IImportSpecifierData,
IFileError,
} from '../types';
import { TangoFile, TangoJsonFile } from './file';
import { TangoRouteModule } from './route-module';
import { TangoStoreModule } from './store-module';
import { TangoServiceModule } from './service-module';
import { IdGenerator } from '../helpers';
import { AppEntryModule } from './entry-module';
import { AbstractViewNode } from './abstract-view-node';
export interface IViewFile {
readonly workspace: IWorkspace;
readonly filename: string;
readonly type: FileType;
/**
* 文件名
*/
filename: string;
/**
* ID 生成器
@@ -55,267 +44,38 @@ export interface IViewFile {
*/
addImportSpecifiers: (source: string, newSpecifiers: IImportSpecifierData[]) => IViewFile;
getNode: (targetNodeId: string) => IViewNode;
getNode: (targetNodeId: string) => AbstractViewNode;
removeNode: (targetNodeId: string) => IViewFile;
removeNode: (targetNodeId: string) => this;
insertChild: (
targetNodeId: string,
newNode: any,
position?: InsertChildPositionType,
) => IViewFile;
insertChild: (targetNodeId: string, newNode: any, position?: InsertChildPositionType) => this;
insertAfter: (targetNodeId: string, newNode: any) => IViewFile;
insertAfter: (targetNodeId: string, newNode: any) => this;
insertBefore: (targetNodeId: string, newNode: any) => IViewFile;
insertBefore: (targetNodeId: string, newNode: any) => this;
replaceNode: (targetNodeId: string, newNode: any) => IViewFile;
replaceNode: (targetNodeId: string, newNode: any) => this;
replaceViewChildren: (rawNodes: any[]) => IViewFile;
replaceViewChildren: (rawNodes: any[]) => this;
updateNodeAttribute: (
nodeId: string,
attrName: string,
attrValue?: any,
relatedImports?: string[],
) => IViewFile;
) => this;
updateNodeAttributes: (
nodeId: string,
config: Record<string, any>,
relatedImports?: string[],
) => IViewFile;
) => this;
get code(): string;
get nodes(): Map<string, IViewNode>;
get nodes(): Map<string, AbstractViewNode>;
get nodesTree(): object[];
get tree(): any;
}
export interface IViewNode {
/**
* 所属的文件
*/
file: IViewFile;
/**
* 节点 ID
*/
readonly id: string;
/**
* 对应的组件
*/
readonly component: string;
/**
* 原始节点对象
*/
readonly rawNode: unknown;
/**
* 属性集合
*/
readonly props: Record<string, any>;
/**
* 克隆原始节点
* @param overrideProps 额外设置给克隆节点的属性
* @returns
*/
cloneRawNode: (overrideProps?: Dict) => unknown;
/**
* 销毁节点
* @returns
*/
destroy: () => void;
/**
* 原始节点的位置信息
*/
get loc(): unknown;
}
export interface IWorkspace {
history: TangoHistory;
selectSource: SelectSource;
dragSource: DragSource;
files: Map<string, TangoFile>;
componentPrototypes: Map<string, IComponentPrototype>;
entry: string;
activeFile: string;
activeViewFile: string;
activeRoute: string;
/**
* 解析后的 tango.config.json 文件,如果要获取项目配置,推荐使用 projectConfig 获取
*/
tangoConfigJson: TangoJsonFile;
/**
* app.js 入口文件解析后的模块
*/
appEntryModule: AppEntryModule;
/**
* 解析后的路由模块
*/
routeModule?: TangoRouteModule;
/**
* 解析后的状态管理模块 Map
*/
storeModules?: Record<string, TangoStoreModule>;
/**
* 解析后的服务模块 Map
*/
serviceModules?: Record<string, TangoServiceModule>;
ready: () => void;
refresh: (names: string[]) => void;
setActiveRoute: (path: string) => void;
setActiveFile: (filename: string) => void;
setComponentPrototypes: (prototypes: Record<string, IComponentPrototype>) => void;
getPrototype: (name: string | IComponentPrototype) => IComponentPrototype;
// ----------------- 文件操作 -----------------
addFiles: (files: IFileConfig[]) => void;
addFile: (filename: string, code: string, fileType?: FileType) => void;
addServiceFile: (serviceName: string, code: string) => void;
addStoreFile: (storeName: string, code: string) => void;
addViewFile: (viewName: string, code: string) => void;
removeFile: (filename: string) => void;
renameFile: (oldFilename: string, newFilename: string) => void;
renameFolder: (oldFoldername: string, newFoldername: string) => void;
/**
* 更新文件
* @param filename 文件名
* @param code 代码
* @param isSyncAst 是否同步 ast
*/
updateFile: (filename: string, code: string, isSyncAst?: boolean) => void;
/**
* 检查并同步文件的 ast
*/
syncFiles: () => void;
listFiles: () => Record<string, string>;
getFile: (filename: string) => TangoFile;
/**
* 文件变化回调
* @param filenames 文件名列表
*/
onFilesChange: (filenames: string[]) => void;
// ----------------- 节点操作 -----------------
removeSelectedNode: () => void;
cloneSelectedNode: () => void;
copySelectedNode: () => void;
pasteSelectedNode: () => void;
insertToSelectedNode: (childNameOrPrototype: string | IComponentPrototype) => void;
insertBeforeSelectedNode: (sourceNameOrPrototype: string | IComponentPrototype) => void;
insertAfterSelectedNode: (sourceNameOrPrototype: string | IComponentPrototype) => void;
dropNode: () => void;
insertToNode: (targetNodeId: string, sourceNameOrPrototype: string | IComponentPrototype) => void;
replaceNode: (targetNodeId: string, sourceNameOrPrototype: string | IComponentPrototype) => void;
updateSelectedNodeAttributes: (
attributes: Record<string, any>,
relatedImports?: string[],
) => void;
/**
* 查询节点
* @param id 节点 ID
* @param module 节点所在的模块名
* @returns 返回节点对象
*/
getNode: (id: string, module?: string) => IViewNode;
// ----------------- 服务函数文件操作 -----------------
getServiceFunction?: (serviceKey: string) => {
name: string;
moduleName: string;
config: Dict<object>;
};
listServiceFunctions?: () => Dict<object>;
removeServiceFunction?: (serviceKey: string) => void;
addServiceFunction?: (serviceName: string, config: Dict, modName?: string) => void;
addServiceFunctions?: (configs: Dict<object>, modName?: string) => void;
updateServiceFunction?: (serviceName: string, payload: Dict, modName?: string) => void;
updateServiceBaseConfig?: (config: Dict, modName?: string) => void;
// ----------------- 状态管理文件操作 -----------------
addStoreState?: (storeName: string, stateName: string, initValue: string) => void;
removeStoreModule?: (storeName: string) => void;
removeStoreVariable?: (variablePath: string) => void;
updateStoreVariable?: (variablePath: string, code: string) => void;
// ----------------- 视图文件操作 -----------------
removeViewModule: (routePath: string) => void;
copyViewPage: (sourceRoutePath: string, targetPageData: IPageConfigData) => void;
// ----------------- 路由文件操作 -----------------
updateRoute: (sourceRoutePath: string, targetPageData: IPageConfigData) => void;
// ----------------- 依赖包操作 -----------------
addDependency?: (data: any) => void;
listDependencies?: () => any;
getDependency?: (pkgName: string) => object;
updateDependency?: (
name: string,
version: string,
options?: {
package?: ITangoConfigPackages;
[x: string]: any;
},
) => void;
removeDependency?: (name: string) => void;
addBizComp?: (
name: string,
version: string,
options?: {
package?: ITangoConfigPackages;
[x: string]: any;
},
) => void;
removeBizComp?: (name: string) => void;
// ----------------- getter -----------------
/**
* 解析后的项目配置信息
*/
get projectConfig(): ITangoConfigJson;
/**
* 当前活动的视图文件
*/
get activeViewModule(): IViewFile;
get pages(): any[];
get bizComps(): string[];
get baseComps(): string[];
get localComps(): string[];
get fileErrors(): IFileError[];
/**
* 是否是有效的项目
* - 包含 tango.config.json
* - 包含视图模块
* - 没有文件错误
*/
get isValid(): boolean;
/**
* 文件中的代码
*/
get code(): string;
}
@@ -1,12 +1,12 @@
import { traverseEntryFile } from '../helpers';
import { IFileConfig } from '../types';
import { IWorkspace } from './interfaces';
import { TangoModule } from './module';
import { AbstractJsFile } from './abstract-js-file';
import { AbstractWorkspace } from './abstract-workspace';
export class AppEntryModule extends TangoModule {
export class JsAppEntryFile extends AbstractJsFile {
routerType: string;
constructor(workspace: IWorkspace, props: IFileConfig) {
constructor(workspace: AbstractWorkspace, props: IFileConfig) {
super(workspace, props, false);
this.update(props.code, true, false);
}
+25
View File
@@ -0,0 +1,25 @@
import { action, computed, makeObservable, observable } from 'mobx';
import { IFileConfig } from '../types';
import { AbstractWorkspace } from './abstract-workspace';
import { AbstractJsFile } from './abstract-js-file';
/**
* 普通 JS 文件
*/
export class JsFile extends AbstractJsFile {
constructor(workspace: AbstractWorkspace, props: IFileConfig) {
super(workspace, props, false);
this.update(props.code, true, false);
makeObservable(this, {
_code: observable,
_cleanCode: observable,
isError: observable,
errorMessage: observable,
code: computed,
cleanCode: computed,
update: action,
updateAst: action,
});
}
}
@@ -1,17 +1,17 @@
import path from 'path';
import { action, computed, makeObservable, observable } from 'mobx';
import { TangoModule } from './module';
import { IWorkspace } from './interfaces';
import { IExportSpecifierData, IFileConfig } from '../types';
import { traverseComponentsEntryFile } from '../helpers';
import { AbstractWorkspace } from './abstract-workspace';
import { AbstractJsFile } from './abstract-js-file';
/**
* '/components/index.js' `/blocks/index.js`
*/
export class TangoComponentsEntryModule extends TangoModule {
export class JsLocalComponentsEntryFile extends AbstractJsFile {
exportList: Record<string, IExportSpecifierData>;
constructor(workspace: IWorkspace, props: IFileConfig) {
constructor(workspace: AbstractWorkspace, props: IFileConfig) {
super(workspace, props, false);
this.update(props.code, true, false);
makeObservable(this, {
@@ -7,20 +7,20 @@ import {
updateRouteToRouteFile,
} from '../helpers';
import { IRouteData, IFileConfig } from '../types';
import { IWorkspace } from './interfaces';
import { TangoModule } from './module';
import { AbstractJsFile } from './abstract-js-file';
import { AbstractCodeWorkspace } from './abstract-code-workspace';
/**
*
*/
export class TangoRouteModule extends TangoModule {
export class JsRouteConfigFile extends AbstractJsFile {
_routes: IRouteData[];
get routes() {
return toJS(this._routes);
}
constructor(workspace: IWorkspace, props: IFileConfig) {
constructor(workspace: AbstractCodeWorkspace, props: IFileConfig) {
super(workspace, props, false);
this.update(props.code, true, false);
@@ -8,13 +8,13 @@ import {
updateBaseConfigToServiceFile,
} from '../helpers';
import { IFileConfig } from '../types';
import { IWorkspace } from './interfaces';
import { TangoModule } from './module';
import { AbstractWorkspace } from './abstract-workspace';
import { AbstractJsFile } from './abstract-js-file';
/**
*
*/
export class TangoServiceModule extends TangoModule {
export class JsServiceFile extends AbstractJsFile {
/**
* index
*/
@@ -39,7 +39,7 @@ export class TangoServiceModule extends TangoModule {
return toJS(this._baseConfig);
}
constructor(workspace: IWorkspace, props: IFileConfig) {
constructor(workspace: AbstractWorkspace, props: IFileConfig) {
super(workspace, props, false);
this.name = getModuleNameByFilename(props.filename);
this.update(props.code, true, false);
@@ -0,0 +1,56 @@
import { action, computed, makeObservable, observable, toJS } from 'mobx';
import { traverseStoreEntryFile, addStoreToEntryFile, removeStoreToEntryFile } from '../helpers';
import { IFileConfig } from '../types';
import { AbstractWorkspace } from './abstract-workspace';
import { AbstractJsFile } from './abstract-js-file';
/**
* stores 入口文件
*/
export class JsStoreEntryFile extends AbstractJsFile {
_stores: string[] = [];
get stores() {
return toJS(this._stores);
}
constructor(workspace: AbstractWorkspace, props: IFileConfig) {
super(workspace, props, false);
this.update(props.code, true, false);
makeObservable(this, {
_stores: observable,
_code: observable,
_cleanCode: observable,
isError: observable,
errorMessage: observable,
stores: computed,
code: computed,
cleanCode: computed,
update: action,
updateAst: action,
});
}
_analysisAst() {
this._stores = traverseStoreEntryFile(this.ast);
}
/**
* 新建模型
* @param name
*/
addStore(name: string) {
this.ast = addStoreToEntryFile(this.ast, name);
return this;
}
/**
* 删除模型
* @param name
*/
removeStore(name: string) {
this.ast = removeStoreToEntryFile(this.ast, name);
return this;
}
}
@@ -1,73 +1,19 @@
import { action, computed, makeObservable, observable, toJS } from 'mobx';
import { action, computed, makeObservable, observable } from 'mobx';
import {
traverseStoreFile,
traverseStoreEntryFile,
addStoreToEntryFile,
getModuleNameByFilename,
addStoreState,
updateStoreState,
removeStoreState,
removeStoreToEntryFile,
} from '../helpers';
import { IFileConfig, IStorePropertyData } from '../types';
import { IWorkspace } from './interfaces';
import { TangoModule } from './module';
/**
*
*/
export class TangoStoreEntryModule extends TangoModule {
_stores: string[] = [];
get stores() {
return toJS(this._stores);
}
constructor(workspace: IWorkspace, props: IFileConfig) {
super(workspace, props, false);
this.update(props.code, true, false);
makeObservable(this, {
_stores: observable,
_code: observable,
_cleanCode: observable,
isError: observable,
errorMessage: observable,
stores: computed,
code: computed,
cleanCode: computed,
update: action,
updateAst: action,
});
}
_analysisAst() {
this._stores = traverseStoreEntryFile(this.ast);
}
/**
*
* @param name
*/
addStore(name: string) {
this.ast = addStoreToEntryFile(this.ast, name);
return this;
}
/**
*
* @param name
*/
removeStore(name: string) {
this.ast = removeStoreToEntryFile(this.ast, name);
return this;
}
}
import { AbstractWorkspace } from './abstract-workspace';
import { AbstractJsFile } from './abstract-js-file';
/**
*
*/
export class TangoStoreModule extends TangoModule {
export class JsStoreFile extends AbstractJsFile {
/**
*
*/
@@ -79,7 +25,7 @@ export class TangoStoreModule extends TangoModule {
actions: IStorePropertyData[];
constructor(workspace: IWorkspace, props: IFileConfig) {
constructor(workspace: AbstractWorkspace, props: IFileConfig) {
super(workspace, props, false);
this.name = getModuleNameByFilename(props.filename);
this.update(props.code, true, false);
@@ -20,18 +20,19 @@ import {
addImportDeclarationLegacy,
updateImportDeclarationLegacy,
} from '../helpers';
import { TangoNode } from './node';
import { JsxViewNode } from './view-node';
import {
IFileConfig,
ITangoViewNodeData,
IViewNodeData,
IImportDeclarationPayload,
InsertChildPositionType,
IImportSpecifierSourceData,
ImportDeclarationDataType,
IImportSpecifierData,
} from '../types';
import { IViewFile, IWorkspace } from './interfaces';
import { TangoModule } from './module';
import { AbstractWorkspace } from './abstract-workspace';
import { IViewFile } from './interfaces';
import { AbstractJsFile } from './abstract-js-file';
/**
* -> map
@@ -56,8 +57,8 @@ function buildImportMap(importedModules: ImportDeclarationDataType) {
* tree data
* @param list
*/
function nodeListToTreeData(list: ITangoViewNodeData[]) {
const map: Record<string, ITangoViewNodeData> = {};
function nodeListToTreeData(list: IViewNodeData[]) {
const map: Record<string, IViewNodeData> = {};
list.forEach((item) => {
// 如果不存在,则初始化
@@ -82,9 +83,9 @@ function nodeListToTreeData(list: ITangoViewNodeData[]) {
/**
*
*/
export class TangoViewModule extends TangoModule implements IViewFile {
export class JsViewFile extends AbstractJsFile implements IViewFile {
// 解析为树结构的 jsxNodes 数组
_nodesTree: ITangoViewNodeData[] = [];
_nodesTree: IViewNodeData[] = [];
/**
*
*/
@@ -108,7 +109,7 @@ export class TangoViewModule extends TangoModule implements IViewFile {
/**
* <id, Node>
*/
private _nodes: Map<string, TangoNode>;
private _nodes: Map<string, JsxViewNode>;
/**
*
* @deprecated
@@ -127,7 +128,7 @@ export class TangoViewModule extends TangoModule implements IViewFile {
return this.ast;
}
constructor(workspace: IWorkspace, props: IFileConfig) {
constructor(workspace: AbstractWorkspace, props: IFileConfig) {
super(workspace, props, false);
this._nodes = new Map();
this.idGenerator = new IdGenerator({ prefix: props.filename });
@@ -176,8 +177,10 @@ export class TangoViewModule extends TangoModule implements IViewFile {
this._codeIdList = [];
nodes.forEach((cur) => {
const node = new TangoNode({
...cur,
const node = new JsxViewNode({
id: cur.id,
component: cur.component,
rawNode: cur.rawNode,
file: this,
});
this._nodes.set(cur.id, node);
+24
View File
@@ -0,0 +1,24 @@
import { action, computed, makeObservable, observable, toJS } from 'mobx';
import type { IFileConfig } from '../types';
import { AbstractWorkspace } from './abstract-workspace';
import { AbstractJsonFile } from './abstract-json-file';
export class JsonFile extends AbstractJsonFile {
get json(): object {
return toJS(this._object);
}
constructor(workspace: AbstractWorkspace, props: IFileConfig) {
super(workspace, props);
makeObservable(this, {
_code: observable,
_cleanCode: observable,
_object: observable,
code: computed,
cleanCode: computed,
json: computed,
update: action,
setValue: action,
});
}
}
-62
View File
@@ -1,62 +0,0 @@
import { JSXElement, SourceLocation } from '@babel/types';
import { Dict } from '@music163/tango-helpers';
import { cloneJSXElement, getJSXElementAttributes } from '../helpers';
import { ITangoViewNodeData } from '../types';
import { TangoViewModule } from './view-module';
import { IViewNode } from './interfaces';
type TangoNodeConstructorPropsType = ITangoViewNodeData & {
file: TangoViewModule;
};
/**
* 视图节点类
*/
export class TangoNode implements IViewNode {
/**
* 节点 ID
*/
readonly id: string;
/**
* 节点对应的组件名
*/
readonly component: string;
readonly rawNode: JSXElement;
/**
* 节点所属的文件对象
*/
file: TangoViewModule;
props: Record<string, any>;
get loc(): SourceLocation {
return this.rawNode?.loc;
}
constructor(props: TangoNodeConstructorPropsType) {
this.file = props.file;
this.id = props.id;
this.component = props.component;
this.rawNode = props.rawNode;
this.props = getJSXElementAttributes(cloneJSXElement(props.rawNode));
}
/**
* 返回克隆后的 ast 节点
* @param overrideProps 额外设置给克隆节点的属性
* @returns
*/
cloneRawNode(overrideProps?: Dict) {
return cloneJSXElement(this.rawNode, overrideProps);
}
/**
* 清空节点的指向,交给 GC 去回收
*/
destroy() {
this.file = null;
}
}
+4 -3
View File
@@ -1,6 +1,7 @@
import { ISelectedItemData, MousePoint } from '@music163/tango-helpers';
import { action, computed, makeObservable, observable, toJS } from 'mobx';
import { IViewFile, IWorkspace } from './interfaces';
import { AbstractWorkspace } from './abstract-workspace';
import { IViewFile } from './interfaces';
type StartDataType = {
point: MousePoint;
@@ -24,7 +25,7 @@ export class SelectSource {
element: null,
};
private readonly workspace: IWorkspace;
private readonly workspace: AbstractWorkspace;
get start() {
return toJS(this._start);
@@ -74,7 +75,7 @@ export class SelectSource {
.filter((node) => !!node);
}
constructor(workspace: IWorkspace) {
constructor(workspace: AbstractWorkspace) {
this.workspace = workspace;
makeObservable(this, {
_items: observable,
+28
View File
@@ -0,0 +1,28 @@
import { JSXElement } from '@babel/types';
import { Dict } from '@music163/tango-helpers';
import { cloneJSXElement, getJSXElementAttributes } from '../helpers';
import { JsViewFile } from './js-view-file';
import { AbstractViewNode, IViewNodeInitConfig } from './abstract-view-node';
/**
* 视图节点类
*/
export class JsxViewNode extends AbstractViewNode<JSXElement, JsViewFile> {
get loc() {
return this.rawNode?.loc;
}
constructor(props: IViewNodeInitConfig<JSXElement, JsViewFile>) {
super(props);
this.props = getJSXElementAttributes(cloneJSXElement(props.rawNode));
}
/**
* 返回克隆后的 ast 节点
* @param overrideProps 额外设置给克隆节点的属性
* @returns
*/
cloneRawNode(overrideProps?: Dict) {
return cloneJSXElement(this.rawNode, overrideProps);
}
}
File diff suppressed because it is too large Load Diff
+19 -31
View File
@@ -6,40 +6,28 @@ export type SimulatorMode = 'desktop' | 'tablet' | 'phone';
* 文件类型枚举
*/
export enum FileType {
// js 文件
Module = 'module',
AppEntryModule = 'appEntryModule',
StoreEntryModule = 'storeEntryModule',
RouteModule = 'routeModule',
ServiceModule = 'serviceModule',
StoreModule = 'storeModule',
File = 'file',
JsFile = 'jsFile',
JsAppEntryFile = 'jsAppEntryFile',
JsRouteConfigFile = 'jsRouteConfigFile',
JsStoreEntryFile = 'jsStoreEntryFile',
JsStoreFile = 'jsStoreFile',
JsServiceFile = 'jsServiceFile',
JsLocalComponentsEntryFile = 'jsLocalComponentsEntryFile',
JsViewFile = 'jsViewFile',
JsonViewFile = 'jsonViewFile',
JsonFile = 'jsonFile',
PackageJsonFile = 'packageJsonFile',
TangoConfigJsonFile = 'tangoConfigJsonFile',
AppJsonFile = 'appJsonFile',
// 组件配置文件
ComponentPrototypeModule = 'componentPrototypeModule',
// 组件运行调试入口文件,一般为 `/app.js`
ComponentDemoEntryModule = 'componentDemoEntryModule',
/**
* 本地组件目录的入口文件
*/
ComponentsEntryModule = 'componentsEntryModule',
/**
* @deprecated 已废弃
*/
BlockEntryModule = 'blockEntryModule',
// jsx 类型视图文件
JsxViewModule = 'jsxViewModule',
// json 类型视图文件
JsonViewModule = 'jsonViewModule',
// 非 js 文件
PackageJson = 'packageJson',
TangoConfigJson = 'tangoConfigJson',
AppJson = 'appJson',
File = 'file',
Json = 'json',
Less = 'less',
Scss = 'scss',
}
export interface IFileConfig {
@@ -65,7 +53,7 @@ export interface IFileError {
/**
* 视图节点数据类型
*/
export interface ITangoViewNodeData<T = JSXElement> {
export interface IViewNodeData<T = JSXElement> {
/**
* 节点 ID
*/
@@ -97,7 +85,7 @@ export interface ITangoViewNodeData<T = JSXElement> {
/**
* 子节点列表
*/
children?: Array<ITangoViewNodeData<T>>;
children?: Array<IViewNodeData<T>>;
}
/**
+5 -5
View File
@@ -179,12 +179,12 @@ describe('string helpers', () => {
});
it('inferFileType', () => {
expect(inferFileType('/src/pages/template.js')).toBe(FileType.JsxViewModule);
expect(inferFileType('/src/pages/template.jsx')).toBe(FileType.JsxViewModule);
expect(inferFileType('/src/pages/template.js')).toBe(FileType.JsViewFile);
expect(inferFileType('/src/pages/template.jsx')).toBe(FileType.JsViewFile);
expect(inferFileType('/src/pages/template.ejs')).toBe(FileType.File);
expect(inferFileType('/src/index.scss')).toBe(FileType.Scss);
expect(inferFileType('/src/index.less')).toBe(FileType.Less);
expect(inferFileType('/src/index.json')).toBe(FileType.Json);
expect(inferFileType('/src/index.scss')).toBe(FileType.File);
expect(inferFileType('/src/index.less')).toBe(FileType.File);
expect(inferFileType('/src/index.json')).toBe(FileType.JsonFile);
});
});
+3 -3
View File
@@ -1,5 +1,5 @@
import React, { useEffect, useMemo } from 'react';
import { DropMethod, FileType, Designer, IWorkspace } from '@music163/tango-core';
import { DropMethod, FileType, Designer, AbstractCodeWorkspace } from '@music163/tango-core';
import { ISelectedItemData, events, getHotkey } from '@music163/tango-helpers';
import {
setElementStyle,
@@ -12,7 +12,7 @@ import { Hotkey } from './hotkey';
import { SelectModeType } from '../types';
interface UseDndProps {
workspace: IWorkspace;
workspace: AbstractCodeWorkspace;
designer: Designer;
/**
* 沙箱内的 DOM 查询操作
@@ -345,7 +345,7 @@ export function useDnd({
// 区块不能拖拽到区块中
if (
workspace.dragSource.prototype.type === 'block' &&
closetDropTargetNode.file.type === FileType.BlockEntryModule
closetDropTargetNode.file.type === FileType.JsLocalComponentsEntryFile
) {
return;
}
+1 -1
View File
@@ -109,7 +109,7 @@ function useSandbox({
// 根据当前 workspace 状态与组件传入的状态是否一致,控制是否需要切换到空白路由
const display = isActive ? 'block' : 'none';
const routePath = isActive ? startRoute || workspace.activeRoute : LANDING_PAGE_PATH;
const routerMode = fixRouterMode(workspace.appEntryModule?.routerType);
const routerMode = fixRouterMode(workspace.jsAppEntryFile?.routerType);
const sandboxProps = isPreview
? {
@@ -294,7 +294,7 @@ export function ExpressionPopover({
height="100%"
showViewButton
dataSource={dataSource || expressionVariables}
appContext={evaluateContext['tango']}
appContext={evaluateContext?.['tango']}
getStoreNames={() => Object.keys(workspace.storeModules)}
serviceModules={serviceModules}
getServiceData={(serviceKey) => {
@@ -21,7 +21,6 @@ import { ColorTag, ConfigGroup, ConfigItem } from '@music163/tango-ui';
import { MinusCircleOutlined, PlusOutlined, QuestionCircleOutlined } from '@ant-design/icons';
import { useBoolean } from '@music163/tango-helpers';
import { isUndefined } from 'lodash-es';
import { Workspace } from '@music163/tango-core';
import { useSandboxQuery } from '../context';
enum DependencyItemType {
@@ -169,7 +168,7 @@ function RenderItem({
}: RenderItemProps) {
const [open, { on, off }] = useBoolean(false);
const workspace = useWorkspace() as Workspace;
const workspace = useWorkspace();
const basePackage = useMemo(() => {
if (type !== DependencyItemType. || !templateBaseDependencies) return undefined;
@@ -4,7 +4,7 @@ import { Box, css } from 'coral-system';
import { IconFont } from '@music163/tango-ui';
import { EyeOutlined, EyeInvisibleOutlined, EllipsisOutlined } from '@ant-design/icons';
import { observer, useWorkspace } from '@music163/tango-context';
import { DropMethod, ITangoViewNodeData } from '@music163/tango-core';
import { DropMethod, IViewNodeData } from '@music163/tango-core';
import { noop, parseDndId } from '@music163/tango-helpers';
import { useSandboxQuery } from '../../context';
import { buildQueryBySlotId } from '../../helpers';
@@ -65,7 +65,7 @@ const filedNames = {
children: 'children',
};
const getNodeKeys = (data: ITangoViewNodeData[]) => {
const getNodeKeys = (data: IViewNodeData[]) => {
const ids: string[] = [];
data?.forEach((node) => {
ids.push(node.id);
@@ -76,7 +76,7 @@ const getNodeKeys = (data: ITangoViewNodeData[]) => {
return ids;
};
const OutlineTreeNode: React.FC<{ node: ITangoViewNodeData } & ComponentsTreeProps> = observer(
const OutlineTreeNode: React.FC<{ node: IViewNodeData } & ComponentsTreeProps> = observer(
({ node, showToggleVisibleIcon, actionItems }) => {
const workspace = useWorkspace();
const sandboxQuery = useSandboxQuery();
@@ -142,7 +142,7 @@ export const ComponentsTree: React.FC<ComponentsTreeProps> = observer(
workspace.selectSource.selected.map((item) => item.id),
);
const file = workspace.activeViewModule;
const nodesTree = (file?.nodesTree ?? []) as ITangoViewNodeData[];
const nodesTree = (file?.nodesTree ?? []) as IViewNodeData[];
const [expandedKeys, setExpandedKeys] = useState(getNodeKeys(nodesTree));
const [contextMenuOpen, setContextMenuOpen] = useState(false);