From eae45003c65b74e01a9c6f465c8ae02f05145cc3 Mon Sep 17 00:00:00 2001 From: Wells Date: Tue, 10 Sep 2024 16:02:53 +0800 Subject: [PATCH] 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 --- packages/context/src/context.ts | 8 +- packages/core/src/factory.ts | 4 +- packages/core/src/helpers/ast/traverse.ts | 4 +- packages/core/src/helpers/string.ts | 34 +- .../src/models/abstract-code-workspace.ts | 264 ++++ packages/core/src/models/abstract-file.ts | 63 + .../models/{module.ts => abstract-js-file.ts} | 32 +- .../core/src/models/abstract-json-file.ts | 106 ++ .../core/src/models/abstract-view-node.ts | 59 + .../core/src/models/abstract-workspace.ts | 1090 ++++++++++++++ packages/core/src/models/designer.ts | 6 +- packages/core/src/models/drag-source.ts | 6 +- packages/core/src/models/drop-target.ts | 6 +- packages/core/src/models/engine.ts | 4 +- packages/core/src/models/file.ts | 198 +-- packages/core/src/models/history.ts | 6 +- packages/core/src/models/index.ts | 34 +- packages/core/src/models/interfaces.ts | 276 +--- .../{entry-module.ts => js-app-entry-file.ts} | 8 +- packages/core/src/models/js-file.ts | 25 + ...e.ts => js-local-components-entry-file.ts} | 8 +- ...oute-module.ts => js-route-config-file.ts} | 8 +- .../{service-module.ts => js-service-file.ts} | 8 +- .../core/src/models/js-store-entry-file.ts | 56 + .../{store-module.ts => js-store-file.ts} | 64 +- .../{view-module.ts => js-view-file.ts} | 27 +- packages/core/src/models/json-file.ts | 24 + packages/core/src/models/node.ts | 62 - packages/core/src/models/select-source.ts | 7 +- packages/core/src/models/view-node.ts | 28 + packages/core/src/models/workspace.ts | 1314 +---------------- packages/core/src/types.ts | 50 +- packages/core/tests/helpers.test.ts | 10 +- packages/designer/src/dnd/use-dnd.ts | 6 +- packages/designer/src/sandbox/sandbox.tsx | 2 +- packages/designer/src/setters/code-setter.tsx | 2 +- .../designer/src/sidebar/dependency-panel.tsx | 3 +- .../sidebar/outline-panel/components-tree.tsx | 8 +- 38 files changed, 1887 insertions(+), 2033 deletions(-) create mode 100644 packages/core/src/models/abstract-code-workspace.ts create mode 100644 packages/core/src/models/abstract-file.ts rename packages/core/src/models/{module.ts => abstract-js-file.ts} (80%) create mode 100644 packages/core/src/models/abstract-json-file.ts create mode 100644 packages/core/src/models/abstract-view-node.ts create mode 100644 packages/core/src/models/abstract-workspace.ts rename packages/core/src/models/{entry-module.ts => js-app-entry-file.ts} (57%) create mode 100644 packages/core/src/models/js-file.ts rename packages/core/src/models/{component-module.ts => js-local-components-entry-file.ts} (81%) rename packages/core/src/models/{route-module.ts => js-route-config-file.ts} (88%) rename packages/core/src/models/{service-module.ts => js-service-file.ts} (91%) create mode 100644 packages/core/src/models/js-store-entry-file.ts rename packages/core/src/models/{store-module.ts => js-store-file.ts} (52%) rename packages/core/src/models/{view-module.ts => js-view-file.ts} (93%) create mode 100644 packages/core/src/models/json-file.ts delete mode 100644 packages/core/src/models/node.ts create mode 100644 packages/core/src/models/view-node.ts diff --git a/packages/context/src/context.ts b/packages/context/src/context.ts index a01a0262..faa761de 100644 --- a/packages/context/src/context.ts +++ b/packages/context/src/context.ts @@ -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 export { TangoEngineProvider }; +/** + * 获取 CodeWorkspace 实例 + * @returns + */ export const useWorkspace = () => { - return useTangoEngine()?.engine.workspace; + return useTangoEngine()?.engine.workspace as AbstractCodeWorkspace; }; export const useDesigner = () => { diff --git a/packages/core/src/factory.ts b/packages/core/src/factory.ts index 58b971c6..f390c1f0 100644 --- a/packages/core/src/factory.ts +++ b/packages/core/src/factory.ts @@ -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; /** * 菜单信息 */ diff --git a/packages/core/src/helpers/ast/traverse.ts b/packages/core/src/helpers/ast/traverse.ts index 2d735953..774a21ca 100644 --- a/packages/core/src/helpers/ast/traverse.ts +++ b/packages/core/src/helpers/ast/traverse.ts @@ -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 = {}; const importedModules: Dict = {}; - const nodes: Array> = []; + const nodes: Array> = []; const cloneAst = t.cloneNode(ast, true, true); const cleanAst = clearTrackingData(cloneAst); const variables: string[] = []; // 使用的 tango 变量 diff --git a/packages/core/src/helpers/string.ts b/packages/core/src/helpers/string.ts index 392e3d0e..57367bca 100644 --- a/packages/core/src/helpers/string.ts +++ b/packages/core/src/helpers/string.ts @@ -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; diff --git a/packages/core/src/models/abstract-code-workspace.ts b/packages/core/src/models/abstract-code-workspace.ts new file mode 100644 index 00000000..4182b6cf --- /dev/null +++ b/packages/core/src/models/abstract-code-workspace.ts @@ -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; + + /** + * 数据服务模块 + */ + serviceModules: Record; + + 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 = {}; + 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, 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(); + } +} diff --git a/packages/core/src/models/abstract-file.ts b/packages/core/src/models/abstract-file.ts new file mode 100644 index 00000000..7de225c9 --- /dev/null +++ b/packages/core/src/models/abstract-file.ts @@ -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; +} diff --git a/packages/core/src/models/module.ts b/packages/core/src/models/abstract-js-file.ts similarity index 80% rename from packages/core/src/models/module.ts rename to packages/core/src/models/abstract-js-file.ts index 7c5c796b..ffdcfbe2 100644 --- a/packages/core/src/models/module.ts +++ b/packages/core/src/models/abstract-js-file.ts @@ -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, - }); - } -} diff --git a/packages/core/src/models/abstract-json-file.ts b/packages/core/src/models/abstract-json-file.ts new file mode 100644 index 00000000..9bb21ffa --- /dev/null +++ b/packages/core/src/models/abstract-json-file.ts @@ -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; + } +} diff --git a/packages/core/src/models/abstract-view-node.ts b/packages/core/src/models/abstract-view-node.ts new file mode 100644 index 00000000..641ff63d --- /dev/null +++ b/packages/core/src/models/abstract-view-node.ts @@ -0,0 +1,59 @@ +import { Dict } from '@music163/tango-helpers'; +import { AbstractFile } from './abstract-file'; + +export interface IViewNodeInitConfig { + id: string; + component: string; + rawNode: RawNodeType; + file: ViewFileType; +} + +export abstract class AbstractViewNode { + /** + * 节点 ID + */ + readonly id: string; + + /** + * 节点对应的组件名 + */ + readonly component: string; + + readonly rawNode: RawNodeType; + + /** + * 节点所属的文件对象 + */ + file: ViewFileType; + + /** + * 节点所属的文件对象 + */ + props: Record; + + /** + * 节点的位置信息 + */ + abstract get loc(): unknown; + + constructor(props: IViewNodeInitConfig) { + 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; +} diff --git a/packages/core/src/models/abstract-workspace.ts b/packages/core/src/models/abstract-workspace.ts new file mode 100644 index 00000000..44d71b13 --- /dev/null +++ b/packages/core/src/models/abstract-workspace.ts @@ -0,0 +1,1090 @@ +import { JSXElement } from '@babel/types'; +import { + IComponentPrototype, + Dict, + ITangoConfigJson, + hasFileExtension, + isString, + logger, + uniq, + setValue, +} from '@music163/tango-helpers'; +import { + prototype2jsxElement, + getFilepath, + isPathnameMatchRoute, + getJSXElementChildrenNames, + namesToImportDeclarations, + prototype2importDeclarationData, +} from '../helpers'; +import { DropMethod } from './drop-target'; +import { HistoryMessage, TangoHistory } from './history'; +import { JsxViewNode } from './view-node'; +import { IFileConfig, FileType, ITangoConfigPackages, IPageConfigData, IFileError } from '../types'; +import { SelectSource } from './select-source'; +import { DragSource } from './drag-source'; +import { JsRouteConfigFile } from './js-route-config-file'; +import { JsViewFile } from './js-view-file'; +import { JsLocalComponentsEntryFile } from './js-local-components-entry-file'; +import { JsAppEntryFile } from './js-app-entry-file'; +import { AbstractFile } from './abstract-file'; +import { JsonFile } from './json-file'; +import { AbstractJsFile } from './abstract-js-file'; + +export interface IWorkspaceInitConfig { + /** + * 入口文件 + */ + entry?: string; + /** + * 初始化文件列表 + */ + files?: IFileConfig[]; + /** + * 默认的激活的路由 + */ + defaultActiveRoute?: string; + /** + * 组件描述列表 + */ + prototypes?: Record; + /** + * 工作区文件变更事件 + */ + onFilesChange?: AbstractWorkspace['onFilesChange']; +} + +/** + * Workspace 抽象基类 + */ +export abstract class AbstractWorkspace extends EventTarget { + /** + * 历史记录 + */ + history: TangoHistory; + /** + * 选中源 + */ + selectSource: SelectSource; + /** + * 拖拽源 + */ + dragSource: DragSource; + + /** + * 工作区的文件列表 + */ + files: Map; + + /** + * 组件配置 + */ + componentPrototypes: Map; + + /** + * 入口文件 + */ + entry: string; + + /** + * 当前路由 + */ + activeRoute: string; + + /** + * 当前选中的文件 + */ + activeFile: string; + + /** + * 当前选中的视图文件 + */ + activeViewFile: string; + + /** + * 应用入口模块 + */ + jsAppEntryFile: JsAppEntryFile; + + /** + * 路由配置模块 + */ + routeModule: JsRouteConfigFile; + + /** + * 组件入口模块 + */ + componentsEntryModule: JsLocalComponentsEntryFile; + + /** + * package.json 文件 + */ + packageJson: JsonFile; + + /** + * tango.config.json 文件 + */ + tangoConfigJson: JsonFile; + + /** + * 绑定事件 + */ + on = this.addEventListener; + + /** + * 移除事件 + */ + off = this.removeEventListener; + + /** + * 工作区是否就绪 + */ + private isReady: boolean; + + /** + * 拷贝的暂存区 + */ + private copyTempNodes: JsxViewNode[]; + + get isValid() { + return !!this.tangoConfigJson && !!this.activeViewModule && this.fileErrors.length === 0; + } + + /** + * 项目配置,返回解析后的 tango.config.json 文件 + */ + get projectConfig() { + return this.tangoConfigJson?.json as ITangoConfigJson; + } + + /** + * 当前激活的视图模块 + */ + get activeViewModule() { + if (!this.activeViewFile) { + this.setActiveViewFile(this.activeRoute); + } + return this.files.get(this.activeViewFile) as JsViewFile; + } + + /** + * 获取页面列表 + */ + get pages() { + const ret: IPageConfigData[] = []; + this.routeModule?.routes.forEach((item) => { + if (item.path !== '*') { + ret.push({ + path: item.path, + name: item.component, + }); + } + }); + return ret; + } + + get bizComps(): string[] { + const packages = this.tangoConfigJson?.getValue('packages'); + let list = this.tangoConfigJson?.getValue('bizDependencies') || []; + if (packages) { + list = [ + ...new Set([ + ...list, + ...Object.keys(packages).filter((e) => packages[e].type === 'bizDependency'), + ]), + ]; + } + return list; + } + + get baseComps(): string[] { + const packages = this.tangoConfigJson?.getValue('packages'); + let list = this.tangoConfigJson?.getValue('baseDependencies') || []; + if (packages) { + list = [ + ...new Set([ + ...list, + ...Object.keys(packages).filter((e) => packages[e].type === 'baseDependency'), + ]), + ]; + } + return list; + } + + get localComps(): string[] { + return Object.keys(this.componentsEntryModule?.exportList || {}); + } + + get fileErrors() { + const errors: IFileError[] = []; + this.files.forEach((file) => { + if (file.isError) { + errors.push({ + filename: file.filename, + message: file.errorMessage, + }); + } + }); + return errors; + } + + constructor(options?: IWorkspaceInitConfig) { + super(); + this.history = new TangoHistory(this); + this.selectSource = new SelectSource(this); + this.dragSource = new DragSource(this); + this.componentPrototypes = new Map(); + this.entry = options?.entry; + this.activeRoute = options?.defaultActiveRoute || '/'; + this.activeFile = options?.entry; + this.activeViewFile = ''; + this.files = new Map(); + this.isReady = false; + + if (options?.onFilesChange) { + // 使用用户提供的 onFilesChange + this.onFilesChange = options.onFilesChange; + } + + if (options?.prototypes) { + this.setComponentPrototypes(options.prototypes); + } + } + + getPrototype(name: string | IComponentPrototype) { + if (isString(name)) { + return this.componentPrototypes.get(name); + } + return name as IComponentPrototype; + } + + /** + * 设置当前路由 + * @param routePath 路由路径 + */ + setActiveRoute(routePath: string) { + if (routePath === this.activeRoute) { + return; + } + this.selectSource.clear(); + this.activeRoute = routePath; + this.setActiveViewFile(routePath); + } + + /** + * 设置当前选中的文件 + * @param filename + */ + setActiveFile(filename: string, isViewFile = false) { + this.activeFile = filename; + if (isViewFile) { + this.activeViewFile = filename; + } + } + + /** + * 根据当前的路由计算当前的视图模块 + */ + setActiveViewFile(routePath: string) { + let filename = this.getFilenameByRoutePath(routePath); + if (!filename) { + // 没有找到 route 对应的文件,使用默认的 entry + for (const [, file] of this.files) { + if (file.type === FileType.JsViewFile) { + filename = file.filename; + break; + } + } + } + if (filename) { + this.setActiveFile(filename, true); + } + } + + setComponentPrototypes(prototypes: Record) { + Object.keys(prototypes).forEach((name) => { + this.componentPrototypes.set(name, prototypes[name]); + }); + } + + /** + * 添加一组文件到工作区,如果文件同名,后面的文件会覆盖前面的文件 + * @param files + */ + addFiles(files: IFileConfig[] = []) { + files.forEach((file) => { + this.addFile(file.filename, file.code, file.type); + }); + } + + /** + * 添加视图文件 + * @param viewName 文件名 + * @param code 代码 + */ + addViewFile(viewName: string, code: string) { + const viewRoute = viewName.startsWith('/') ? viewName : `/${viewName}`; + const filename = `/src/pages/${viewName}.js`; + this.addFile(filename, code); + this.addRoute( + { + name: viewName, + path: viewRoute, + }, + filename, + ); + } + + updateFile(filename: string, code: string, isSyncAst = true) { + const file = this.getFile(filename); + if (file instanceof AbstractJsFile) { + file.update(code, isSyncAst); + } else { + file.update(code); + } + + const shouldFormat = this.projectConfig?.designerConfig?.autoFormatCode; + if (shouldFormat && file instanceof JsViewFile) { + file.removeUnusedImportSpecifiers().update(); + } + this.history.push({ + message: HistoryMessage.UpdateCode, + data: { + [filename]: code, + }, + }); + } + + syncFiles() { + this.files.forEach((file) => { + if (file instanceof AbstractJsFile) { + file.updateAst(); + } + }); + } + + removeFile(filename: string) { + if (this.files.get(filename)) { + // 如果是文件,直接删除 + this.files.delete(filename); + } else { + // 没有匹配到,就是一个目录,直接删除整个目录 + // FIXME: 可能存在风险,如果文件夹中的模块被复用,则会导致误删除 + Array.from(this.files.keys()).forEach((key) => { + if (key.startsWith(`${filename}/`)) { + this.files.delete(key); + } + }); + } + } + + /** + * 重命名文件 + * @param oldFilename + * @param newFilename + */ + renameFile(oldFilename: string, newFilename: string) { + const file = this.files.get(oldFilename); + if (file) { + this.removeFile(oldFilename); + this.addFile(newFilename, file.code); + } + } + + /** + * 重命名文件夹 + * @param oldFoldername 旧文件夹名 + * @param newFoldername 新文件夹名 + */ + renameFolder(oldFoldername: string, newFoldername: string) { + Array.from(this.files.keys()).forEach((key) => { + if (key.startsWith(`${oldFoldername}/`)) { + const newKey = key.replace(oldFoldername, newFoldername); + this.renameFile(key, newKey); + } + }); + } + + /** + * 根据文件名获取文件对象 + * @param filename + * @returns + */ + getFile(filename: string) { + return this.files.get(filename); + } + + /** + * 获取文件列表 + * @returns { [filename]: fileCode } + */ + listFiles() { + const ret: Dict = {}; + this.files.forEach((file) => { + ret[file.filename] = file.cleanCode; + }); + return ret; + } + + /** + * 删除视图模块 + * @param route 路由名称 + */ + removeViewModule(routePath: string) { + // get filename first + const filename = this.getFilenameByRoutePath(routePath); + if (this.routeModule) { + this.routeModule.removeRoute(routePath).update(); + this.setActiveRoute(this.routeModule.routes[0]?.path || '/'); + } + this.removeFile(filename); + } + + /** + * 添加新的路由 + */ + addRoute(routeData: IPageConfigData, importFilePath: string) { + this.routeModule?.addRoute(routeData.path, importFilePath).update(); + } + + /** + * 更新页面路由配置 + * @param sourceRoutePath + * @param targetPageData + */ + updateRoute(sourceRoutePath: string, targetPageData: IPageConfigData) { + if (sourceRoutePath !== targetPageData.path) { + this.routeModule?.updateRoute(sourceRoutePath, targetPageData.path).update(); + } + } + + /** + * 复制视图文件 + * @param sourceRoute + * @param targetRouteConfig + */ + copyViewPage(sourceRoutePath: string, targetPageData: IPageConfigData) { + const sourceFilePath = this.getRealViewFilePath(this.getFilenameByRoutePath(sourceRoutePath)); + const targetFilePath = getFilepath(targetPageData.path, '/src/pages'); + this.copyFiles(sourceFilePath, targetFilePath); + this.addRoute(targetPageData, targetFilePath); + } + + getNode(id: string, filename?: string) { + const file = filename ? this.getFile(filename) : this.activeViewModule; + if (file instanceof JsViewFile) { + return file.getNode(id); + } + } + + /** + * 应用代码初始化完成 + */ + ready() { + if (!this.isReady) { + this.isReady = true; + + this.history.push({ + message: HistoryMessage.InitView, + data: { + [this.activeViewModule?.filename]: this.activeViewModule?.code, + }, + }); + } + } + + addDependency(data: any) { + // TODO: implement it to replace addBizComp & addServiceComp + } + + /** + * 获取 package.json 中的依赖列表 + * @returns + * TODO: fix this logic to merge dependencies from package.json and tango.config.json + */ + listDependencies() { + return this.packageJson?.getValue('dependencies'); + } + + getDependency(pkgName: string) { + const packages = this.tangoConfigJson?.getValue('packages'); + const dependencies = this.packageJson?.getValue('dependencies'); // 兼容老版本 + const detail = { + version: dependencies?.[pkgName], + ...(packages?.[pkgName] || {}), + }; + return detail; + } + + /** + * 更新依赖,没有就添加 + * @param name + * @param version + * FIXME: 参数3的设置需要重新考虑下 + */ + updateDependency( + name: string, + version: string, + options?: { + package?: ITangoConfigPackages; + [x: string]: any; + }, + ) { + this.packageJson + ?.setValue('dependencies', (deps = {}) => { + deps[name] = version; + return deps; + }) + .update(); + + this.tangoConfigJson + ?.setValue('packages', (packages) => { + // 兼容以前的逻辑,只在拥有 package 参数时,才会更新 packages 字段 + if (!packages) { + return undefined; + } + + setValue(packages, name, { + ...packages[name], // 保留原有的配置 + version, // 更新版本号 + ...options?.package, // 更新 package 配置 + }); + + return packages; + }) + .update(); + + this.history.push({ + message: HistoryMessage.UpdateDependency, + data: { + [this.packageJson.filename]: this.packageJson.code, + }, + }); + } + + /** + * 移除依赖 + * @param name + */ + removeDependency(name: string) { + this.packageJson + ?.setValue('dependencies', (deps) => { + if (deps[name]) { + delete deps[name]; + } + return deps; + }) + .update(); + + this.tangoConfigJson + ?.setValue('packages', (packages = {}) => { + if (packages?.[name]) { + delete packages[name]; + } + + return packages; + }) + .update(); + + this.history.push({ + message: HistoryMessage.RemoveDependency, + data: { + [this.packageJson.filename]: this.packageJson.code, + }, + }); + } + + /** + * 删除业务组件 + * @param name + */ + removeBizComp(name: string) { + this.tangoConfigJson + ?.setValue('bizDependencies', (deps?: string[]) => { + if (!deps) { + return undefined; + } + return deps.filter((dep) => dep !== name); + }) + .update(); + this.removeDependency(name); + } + + /** + * 添加业务组件 + * @param name + */ + addBizComp( + name: string, + version: string, + options?: { + package?: ITangoConfigPackages; + [x: string]: any; + }, + ) { + const packages = this.tangoConfigJson.getValue('packages'); + this.updateDependency(name, version, { + ...options, + ...(!!packages && { + package: { + ...options?.package, + type: 'bizDependency', + }, + }), + }); + + // 兼容以前的逻辑 + if (!options?.package && !packages) { + // TODO: if tangoConfigJson not found, init this file + this.tangoConfigJson + ?.setValue('bizDependencies', (deps: string[] = []) => { + if (!deps.includes(name)) { + deps.push(name); + } + return deps; + }) + .update(); + } + + this.tangoConfigJson && + this.history.push({ + message: HistoryMessage.UpdateDependency, + data: { + [this.tangoConfigJson.filename]: this.tangoConfigJson.code, + }, + }); + } + + /** + * 删除选中节点 + */ + removeSelectedNode() { + const file = this.selectSource.file; + if (!file) return; + + // 选中的结点一定位于相同的文件中 + this.selectSource.nodes.forEach((node) => { + file.removeNode(node.id); + }); + file.update(); + this.selectSource.clear(); + this.history.push({ + message: HistoryMessage.RemoveNode, + data: { + [file.filename]: file.code, + }, + }); + } + + /** + * 复制选中结点 + */ + copySelectedNode() { + this.copyTempNodes = this.selectSource.nodes as JsxViewNode[]; + } + + /** + * 粘贴选中结点 + * @deprecated 考虑废弃 + * TODO: 重构该逻辑,抽离出公共的方法 + */ + pasteSelectedNode() { + if (this.selectSource.size !== 1) return; + if (!this.copyTempNodes) return; + + // TODO: 潜在隐患,如果跨页的话,代码里的逻辑调用也要处理 + + const importDeclarations = this.getImportDeclarationByNodes( + this.copyTempNodes.map((node) => node.rawNode), + ); + + importDeclarations.forEach((importDeclaration) => { + this.activeViewModule.updateImportSpecifiersLegacy(importDeclaration); + }); + + this.copyTempNodes.forEach((node) => { + this.activeViewModule.insertAfter(this.selectSource.first.id, node.cloneRawNode()); + }); + + this.activeViewModule.update(); + + this.history.push({ + message: HistoryMessage.CloneNode, + data: { + [this.activeViewModule.filename]: this.activeViewModule.code, + }, + }); + } + + /** + * 克隆选中节点,追加到当前结点的后方 + */ + cloneSelectedNode() { + const file = this.selectSource.file; + + let injectedProps; + if (this.projectConfig?.designerConfig?.autoGenerateComponentId) { + // 生成新的组件 id + injectedProps = { + tid: file.idGenerator.generateId(this.selectSource.firstNode.component).id, + }; + } + + file + .insertAfter( + this.selectSource.first.id, + this.selectSource.firstNode.cloneRawNode(injectedProps) as JSXElement, + ) + .update(); + + this.history.push({ + message: HistoryMessage.CloneNode, + data: { + [file.filename]: file.code, + }, + }); + } + + /** + * 在目标节点中插入子节点 + * @param targetNodeId 目标节点dnd-id + * @param sourceName 插入的组件名称 + * @returns + */ + insertToNode(targetNodeId: string, sourceName: string | IComponentPrototype) { + if (!targetNodeId || !sourceName) { + return; + } + + const sourcePrototype = this.getPrototype(sourceName); + const newNode = prototype2jsxElement(sourcePrototype); + const file = this.getNode(targetNodeId).file; + const { source, specifiers } = prototype2importDeclarationData(sourcePrototype, file.filename); + file + .insertChild(targetNodeId, newNode, 'last') + .addImportSpecifiers(source, specifiers) + .update(); + this.history.push({ + message: HistoryMessage.InsertNode, + data: { + [file.filename]: file.code, + }, + }); + } + + /** + * 替换目标节点 + */ + replaceNode(targetNodeId: string, sourceName: string | IComponentPrototype) { + if (!targetNodeId || !sourceName) { + return; + } + + const sourcePrototype = this.getPrototype(sourceName); + const newNode = prototype2jsxElement(sourcePrototype); + const file = this.getNode(targetNodeId).file; + const { source, specifiers } = prototype2importDeclarationData(sourcePrototype, file.filename); + file.replaceNode(targetNodeId, newNode).addImportSpecifiers(source, specifiers).update(); + this.history.push({ + message: HistoryMessage.ReplaceNode, + data: { + [file.filename]: file.code, + }, + }); + } + + /** + * 在选中节点中插入子节点 + * @param childName 节点名 + */ + insertToSelectedNode(childName: string | IComponentPrototype) { + const insertedPrototype = this.getPrototype(childName); + if (insertedPrototype) { + const newNode = prototype2jsxElement(insertedPrototype); + const file = this.selectSource.file; + const { source, specifiers } = prototype2importDeclarationData( + insertedPrototype, + file.filename, + ); + file + .insertChild(this.selectSource.first.id, newNode, 'last') + .addImportSpecifiers(source, specifiers) + .update(); + this.history.push({ + message: HistoryMessage.InsertNode, + data: { + [file.filename]: file.code, + }, + }); + } + } + + insertBeforeSelectedNode(sourceName: string | IComponentPrototype) { + if (!sourceName) { + return; + } + const targetNodeId = this.selectSource.first.id; + const sourcePrototype = this.getPrototype(sourceName); + const newNode = prototype2jsxElement(sourcePrototype); + const file = this.getNode(targetNodeId).file; + const { source, specifiers } = prototype2importDeclarationData(sourcePrototype, file.filename); + + file.insertBefore(targetNodeId, newNode).addImportSpecifiers(source, specifiers).update(); + + this.history.push({ + message: HistoryMessage.InsertBeforeNode, + data: { + [file.filename]: file.code, + }, + }); + } + + insertAfterSelectedNode(sourceName: string | IComponentPrototype) { + if (!sourceName) { + return; + } + const targetNodeId = this.selectSource.first.id; + const sourcePrototype = this.getPrototype(sourceName); + const newNode = prototype2jsxElement(sourcePrototype); + const file = this.getNode(targetNodeId).file; + const { source, specifiers } = prototype2importDeclarationData(sourcePrototype, file.filename); + + file.insertAfter(targetNodeId, newNode).addImportSpecifiers(source, specifiers).update(); + + this.history.push({ + message: HistoryMessage.InsertAfterNode, + data: { + [file.filename]: file.code, + }, + }); + } + + updateSelectedNodeAttributes( + attributes: Record = {}, + relatedImports: string[] = [], + ) { + const file = this.selectSource.file; + file.updateNodeAttributes(this.selectSource.first.id, attributes, relatedImports).update(); + this.history.push({ + message: HistoryMessage.UpdateAttribute, + data: { + [file.filename]: file.code, + }, + }); + } + + /** + * 将节点拽入视图中 + */ + dropNode() { + const dragSource = this.dragSource; + const dropTarget = dragSource.dropTarget; + + if (!dragSource.prototype || !dropTarget.id) { + // 无效的 drag source 或 drop target,提前退出 + logger.error('invalid dragSource or dropTarget'); + return; + } + + // TODO: 这里需要一个额外的信息,DropTarget 的最近容器节点,用于判断目标元素是否可以被置入容器中 + const dragSourcePrototype = dragSource.prototype; + + let injectedProps; // 额外注入给节点的属性 + if (this.projectConfig?.designerConfig?.autoGenerateComponentId) { + injectedProps = { + tid: this.activeViewModule.idGenerator.generateId(dragSource.prototype.name).id, + }; + } + + let newNode; + if (dragSource.id) { + // 来自画布,完整的克隆该节点 + newNode = dragSource.getNode().cloneRawNode(); + } else { + // 来自物料面板,创建新的初始化节点 + newNode = prototype2jsxElement(dragSource.prototype, injectedProps); + } + + if (!newNode) { + return; + } + + const targetFile = dropTarget.node?.file; + const sourceFile = dragSource.node?.file; + + // dragSourcePrototype to importDeclarations + const { source, specifiers } = prototype2importDeclarationData( + dragSourcePrototype, + targetFile.filename, + ); + + let isValidOperation = true; + switch (dropTarget.method) { + // 直接往目标节点的 children 里添加一个节点 + case DropMethod.InsertChild: { + targetFile + .insertChild(dropTarget.id, newNode, 'last') + .addImportSpecifiers(source, specifiers); + break; + } + case DropMethod.InsertFirstChild: { + targetFile + .insertChild(dropTarget.id, newNode, 'first') + .addImportSpecifiers(source, specifiers); + break; + } + // 往目标节点的后边插入一个节点 + case DropMethod.InsertAfter: { + targetFile.insertAfter(dropTarget.id, newNode).addImportSpecifiers(source, specifiers); + break; + } + // 往目标节点的前方插入一个节点 + case DropMethod.InsertBefore: { + targetFile.insertBefore(dropTarget.id, newNode).addImportSpecifiers(source, specifiers); + break; + } + // 替换目标节点 + case DropMethod.ReplaceNode: { + targetFile.replaceNode(dropTarget.id, newNode).addImportSpecifiers(source, specifiers); + break; + } + default: + isValidOperation = false; + break; + } + + // 如果拖拽来源有 ID,表示来自画布 + const isDraggingFromView = !!dragSource.id; + + if (isValidOperation) { + if (isDraggingFromView) { + sourceFile.removeNode(dragSource.id); + } + + this.selectSource.clear(); + } + + targetFile.update(); + if (isDraggingFromView && sourceFile.filename !== targetFile.filename) { + sourceFile.update(); + } + + dragSource.clear(); + + if (isValidOperation) { + this.history.push({ + message: HistoryMessage.DropNode, + data: { + [targetFile.filename]: targetFile.code, + }, + }); + } + } + + onFilesChange(filenams: string[]) { + // do nothing + } + + /** + * 刷新目标文件 + * @param filenames + */ + refresh(filenames: string[]) { + this.dispatchEvent( + new CustomEvent('refresh', { + detail: { + filenames, + entry: this.entry, + }, + }), + ); + } + + /** + * 基于输入结点获得结点依赖的导入声明信息 + * @param nodes + */ + private getImportDeclarationByNodes(nodes: JSXElement[]) { + let names = nodes.reduce((prev, cur) => { + prev = prev.concat(getJSXElementChildrenNames(cur)); + return prev; + }, []); + names = uniq(names); + const importDeclarations = namesToImportDeclarations(names, this.selectSource.file.importMap); + return importDeclarations; + } + + /** + * 根据路由路径获取文件名 + * @param routePath + * @returns + */ + private getFilenameByRoutePath(routePath: string) { + let filename: string; + this.routeModule?.routes.forEach((route) => { + if (isPathnameMatchRoute(routePath, route.path) && route.importPath) { + if (route.importPath.startsWith('@/')) { + filename = route.importPath; + const alias = this.tangoConfigJson.getValue('sandbox.alias') || {}; + if (alias['@']) { + filename = filename.replace('@', alias['@']); + } + filename = this.getRealViewFilePath(filename); + } else { + const absolutePath = route.importPath.replace('.', '/src'); + filename = this.getRealViewFilePath(absolutePath); + } + } + }); + return filename; + } + + private getRealViewFilePath(filePath: string): string { + // 如果有后缀名直接返回 + if (hasFileExtension(filePath)) { + return filePath; + } + + const possiblePaths = [ + `${filePath}.js`, + `${filePath}.jsx`, + `${filePath}/index.js`, + `${filePath}/index.jsx`, + ]; + + for (const filepath of possiblePaths) { + if (this.files.has(filepath)) { + return filepath; + } + } + } + + /** + * 文件拷贝 + * @param sourcePath + * @param targetPath + */ + private copyFiles(sourceFilePath: string, targetFilePath: string) { + if (this.files.has(sourceFilePath)) { + // 来源是文件 + const file = this.files.get(sourceFilePath); + this.addFile(`${targetFilePath}.js`, file.cleanCode, file.type); + } else if (this.files.has(`${sourceFilePath}/index.js`)) { + // 来源是目录 + Array.from(this.files.keys()).forEach((key) => { + if (key.startsWith(`${sourceFilePath}/`)) { + const sourceFile = this.getFile(key); + this.addFile( + targetFilePath + key.slice(sourceFilePath.length), + sourceFile.cleanCode, + sourceFile.type, + ); + } + }); + } else { + logger.error('copyFiles failed, source: %s, target: %s', sourceFilePath, targetFilePath); + } + } + + abstract addFile(filename: string, code: string, fileType?: FileType): void; +} diff --git a/packages/core/src/models/designer.ts b/packages/core/src/models/designer.ts index f8989e98..533d511d 100644 --- a/packages/core/src/models/designer.ts +++ b/packages/core/src/models/designer.ts @@ -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); diff --git a/packages/core/src/models/drag-source.ts b/packages/core/src/models/drag-source.ts index 29373a51..cdf89753 100644 --- a/packages/core/src/models/drag-source.ts +++ b/packages/core/src/models/drag-source.ts @@ -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; diff --git a/packages/core/src/models/drop-target.ts b/packages/core/src/models/drop-target.ts index 6f2083c4..9ae0466e 100644 --- a/packages/core/src/models/drop-target.ts +++ b/packages/core/src/models/drop-target.ts @@ -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; diff --git a/packages/core/src/models/engine.ts b/packages/core/src/models/engine.ts index 0db4bdec..84f6b566 100644 --- a/packages/core/src/models/engine.ts +++ b/packages/core/src/models/engine.ts @@ -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; /** * 设计器状态 */ diff --git a/packages/core/src/models/file.ts b/packages/core/src/models/file.ts index 48416757..18150c44 100644 --- a/packages/core/src/models/file.ts +++ b/packages/core/src/models/file.ts @@ -1,206 +1,28 @@ -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'; - -/** - * 普通文件,不进行 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; - } - - 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(); - this._code = code; - this._cleanCode = code; - } - 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) { +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'; + +export class TangoFile extends AbstractFile { + constructor(workspace: AbstractWorkspace, 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; - } + if (!isNil(code)) { + this.lastModified = Date.now(); 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; - } } diff --git a/packages/core/src/models/history.ts b/packages/core/src/models/history.ts index 1576303d..5db6acc1 100644 --- a/packages/core/src/models/history.ts +++ b/packages/core/src/models/history.ts @@ -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, { diff --git a/packages/core/src/models/index.ts b/packages/core/src/models/index.ts index 92bdb05b..e279bced 100644 --- a/packages/core/src/models/index.ts +++ b/packages/core/src/models/index.ts @@ -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'; diff --git a/packages/core/src/models/interfaces.ts b/packages/core/src/models/interfaces.ts index 92136ead..1ad60728 100644 --- a/packages/core/src/models/interfaces.ts +++ b/packages/core/src/models/interfaces.ts @@ -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, relatedImports?: string[], - ) => IViewFile; + ) => this; - get code(): string; - get nodes(): Map; + get nodes(): Map; get nodesTree(): object[]; get tree(): any; -} - -export interface IViewNode { - /** - * 所属的文件 - */ - file: IViewFile; - - /** - * 节点 ID - */ - readonly id: string; - /** - * 对应的组件 + * 文件中的代码 */ - readonly component: string; - - /** - * 原始节点对象 - */ - readonly rawNode: unknown; - - /** - * 属性集合 - */ - readonly props: Record; - - /** - * 克隆原始节点 - * @param overrideProps 额外设置给克隆节点的属性 - * @returns - */ - cloneRawNode: (overrideProps?: Dict) => unknown; - - /** - * 销毁节点 - * @returns - */ - destroy: () => void; - - /** - * 原始节点的位置信息 - */ - get loc(): unknown; -} - -export interface IWorkspace { - history: TangoHistory; - selectSource: SelectSource; - dragSource: DragSource; - - files: Map; - componentPrototypes: Map; - - entry: string; - activeFile: string; - activeViewFile: string; - activeRoute: string; - - /** - * 解析后的 tango.config.json 文件,如果要获取项目配置,推荐使用 projectConfig 获取 - */ - tangoConfigJson: TangoJsonFile; - /** - * app.js 入口文件解析后的模块 - */ - appEntryModule: AppEntryModule; - /** - * 解析后的路由模块 - */ - routeModule?: TangoRouteModule; - /** - * 解析后的状态管理模块 Map - */ - storeModules?: Record; - /** - * 解析后的服务模块 Map - */ - serviceModules?: Record; - - ready: () => void; - refresh: (names: string[]) => void; - - setActiveRoute: (path: string) => void; - setActiveFile: (filename: string) => void; - - setComponentPrototypes: (prototypes: Record) => 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; - 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, - relatedImports?: string[], - ) => void; - - /** - * 查询节点 - * @param id 节点 ID - * @param module 节点所在的模块名 - * @returns 返回节点对象 - */ - getNode: (id: string, module?: string) => IViewNode; - - // ----------------- 服务函数文件操作 ----------------- - - getServiceFunction?: (serviceKey: string) => { - name: string; - moduleName: string; - config: Dict; - }; - listServiceFunctions?: () => Dict; - removeServiceFunction?: (serviceKey: string) => void; - addServiceFunction?: (serviceName: string, config: Dict, modName?: string) => void; - addServiceFunctions?: (configs: Dict, 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; } diff --git a/packages/core/src/models/entry-module.ts b/packages/core/src/models/js-app-entry-file.ts similarity index 57% rename from packages/core/src/models/entry-module.ts rename to packages/core/src/models/js-app-entry-file.ts index efa844ed..ecc6dc8e 100644 --- a/packages/core/src/models/entry-module.ts +++ b/packages/core/src/models/js-app-entry-file.ts @@ -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); } diff --git a/packages/core/src/models/js-file.ts b/packages/core/src/models/js-file.ts new file mode 100644 index 00000000..5bc18bd4 --- /dev/null +++ b/packages/core/src/models/js-file.ts @@ -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, + }); + } +} diff --git a/packages/core/src/models/component-module.ts b/packages/core/src/models/js-local-components-entry-file.ts similarity index 81% rename from packages/core/src/models/component-module.ts rename to packages/core/src/models/js-local-components-entry-file.ts index 3560010f..3e3674e1 100644 --- a/packages/core/src/models/component-module.ts +++ b/packages/core/src/models/js-local-components-entry-file.ts @@ -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; - constructor(workspace: IWorkspace, props: IFileConfig) { + constructor(workspace: AbstractWorkspace, props: IFileConfig) { super(workspace, props, false); this.update(props.code, true, false); makeObservable(this, { diff --git a/packages/core/src/models/route-module.ts b/packages/core/src/models/js-route-config-file.ts similarity index 88% rename from packages/core/src/models/route-module.ts rename to packages/core/src/models/js-route-config-file.ts index 78cb1328..4cded502 100644 --- a/packages/core/src/models/route-module.ts +++ b/packages/core/src/models/js-route-config-file.ts @@ -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); diff --git a/packages/core/src/models/service-module.ts b/packages/core/src/models/js-service-file.ts similarity index 91% rename from packages/core/src/models/service-module.ts rename to packages/core/src/models/js-service-file.ts index 8d42e7af..562de3ae 100644 --- a/packages/core/src/models/service-module.ts +++ b/packages/core/src/models/js-service-file.ts @@ -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); diff --git a/packages/core/src/models/js-store-entry-file.ts b/packages/core/src/models/js-store-entry-file.ts new file mode 100644 index 00000000..5566be16 --- /dev/null +++ b/packages/core/src/models/js-store-entry-file.ts @@ -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; + } +} diff --git a/packages/core/src/models/store-module.ts b/packages/core/src/models/js-store-file.ts similarity index 52% rename from packages/core/src/models/store-module.ts rename to packages/core/src/models/js-store-file.ts index 4b88084a..49b9ab68 100644 --- a/packages/core/src/models/store-module.ts +++ b/packages/core/src/models/js-store-file.ts @@ -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); diff --git a/packages/core/src/models/view-module.ts b/packages/core/src/models/js-view-file.ts similarity index 93% rename from packages/core/src/models/view-module.ts rename to packages/core/src/models/js-view-file.ts index aff84f6f..2b6f2cae 100644 --- a/packages/core/src/models/view-module.ts +++ b/packages/core/src/models/js-view-file.ts @@ -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 = {}; +function nodeListToTreeData(list: IViewNodeData[]) { + const map: Record = {}; 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 { /** * 节点列表 */ - private _nodes: Map; + private _nodes: Map; /** * 导入的模块 * @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); diff --git a/packages/core/src/models/json-file.ts b/packages/core/src/models/json-file.ts new file mode 100644 index 00000000..54254166 --- /dev/null +++ b/packages/core/src/models/json-file.ts @@ -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, + }); + } +} diff --git a/packages/core/src/models/node.ts b/packages/core/src/models/node.ts deleted file mode 100644 index 57182c0b..00000000 --- a/packages/core/src/models/node.ts +++ /dev/null @@ -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; - - 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; - } -} diff --git a/packages/core/src/models/select-source.ts b/packages/core/src/models/select-source.ts index b1542bd4..f0553910 100644 --- a/packages/core/src/models/select-source.ts +++ b/packages/core/src/models/select-source.ts @@ -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, diff --git a/packages/core/src/models/view-node.ts b/packages/core/src/models/view-node.ts new file mode 100644 index 00000000..08ddc75f --- /dev/null +++ b/packages/core/src/models/view-node.ts @@ -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 { + get loc() { + return this.rawNode?.loc; + } + + constructor(props: IViewNodeInitConfig) { + super(props); + this.props = getJSXElementAttributes(cloneJSXElement(props.rawNode)); + } + + /** + * 返回克隆后的 ast 节点 + * @param overrideProps 额外设置给克隆节点的属性 + * @returns + */ + cloneRawNode(overrideProps?: Dict) { + return cloneJSXElement(this.rawNode, overrideProps); + } +} diff --git a/packages/core/src/models/workspace.ts b/packages/core/src/models/workspace.ts index f90a5195..3d21fec8 100644 --- a/packages/core/src/models/workspace.ts +++ b/packages/core/src/models/workspace.ts @@ -1,264 +1,14 @@ import { action, computed, makeObservable, observable } from 'mobx'; -import { JSXElement } from '@babel/types'; -import { - IComponentPrototype, - Dict, - ITangoConfigJson, - hasFileExtension, - isStoreVariablePath, - isString, - logger, - parseServiceVariablePath, - parseStoreVariablePath, - uniq, - setValue, -} from '@music163/tango-helpers'; -import { - prototype2jsxElement, - inferFileType, - getFilepath, - isPathnameMatchRoute, - getJSXElementChildrenNames, - namesToImportDeclarations, - prototype2importDeclarationData, -} from '../helpers'; -import { DropMethod } from './drop-target'; -import { HistoryMessage, TangoHistory } from './history'; -import { TangoNode } from './node'; -import { TangoJsModule, TangoModule } from './module'; -import { TangoFile, TangoJsonFile, TangoLessFile } from './file'; -import { IWorkspace } from './interfaces'; -import { IFileConfig, FileType, ITangoConfigPackages, IPageConfigData, IFileError } from '../types'; -import { SelectSource } from './select-source'; -import { DragSource } from './drag-source'; -import { TangoRouteModule } from './route-module'; -import { TangoStoreEntryModule, TangoStoreModule } from './store-module'; -import { TangoServiceModule } from './service-module'; -import { TangoViewModule } from './view-module'; -import { TangoComponentsEntryModule } from './component-module'; -import { AppEntryModule } from './entry-module'; - -export interface IWorkspaceOptions { - /** - * 入口文件 - */ - entry?: string; - /** - * 初始化文件列表 - */ - files?: IFileConfig[]; - /** - * 默认的激活的路由 - */ - defaultActiveRoute?: string; - /** - * 组件描述列表 - */ - prototypes?: Record; - /** - * 工作区文件变更事件 - */ - onFilesChange?: IWorkspace['onFilesChange']; -} +import { IWorkspaceInitConfig } from './abstract-workspace'; +import { AbstractCodeWorkspace } from './abstract-code-workspace'; +import { FileType } from '../types'; /** * 工作区 */ -export class Workspace extends EventTarget implements IWorkspace { - history: TangoHistory; - selectSource: SelectSource; - dragSource: DragSource; - - /** - * 工作区的文件列表 - */ - files: Map; - - /** - * 组件配置 - */ - componentPrototypes: Map; - - /** - * 入口文件 - */ - entry: string; - - /** - * 当前路由 - */ - activeRoute: string; - - /** - * 当前选中的文件 - */ - activeFile: string; - - /** - * 当前选中的视图文件 - */ - activeViewFile: string; - - /** - * 应用入口模块 - */ - appEntryModule: AppEntryModule; - - /** - * 路由配置模块 - */ - routeModule: TangoRouteModule; - - /** - * 模型入口配置模块 - */ - storeEntryModule: TangoStoreEntryModule; - - storeModules: Record = {}; - - serviceModules: Record = {}; - - componentsEntryModule: TangoComponentsEntryModule; - - /** - * package.json 文件 - */ - packageJson: TangoJsonFile; - - /** - * tango.config.json 文件 - */ - tangoConfigJson: TangoJsonFile; - - /** - * 绑定事件 - * TODO: 是否需要自己来管理 listeners,并及时进行 gc - */ - on = this.addEventListener; - - /** - * 移除事件 - */ - off = this.removeEventListener; - - /** - * 工作区是否就绪 - */ - private isReady: boolean; - - /** - * 拷贝的暂存区 - */ - private copyTempNodes: TangoNode[]; - - get isValid() { - return !!this.tangoConfigJson && !!this.activeViewModule && this.fileErrors.length === 0; - } - - /** - * 项目配置,返回解析后的 tango.config.json 文件 - */ - get projectConfig() { - return this.tangoConfigJson?.json as ITangoConfigJson; - } - - /** - * 当前激活的视图模块 - */ - get activeViewModule() { - if (!this.activeViewFile) { - this.setActiveViewFile(this.activeRoute); - } - return this.files.get(this.activeViewFile) as TangoViewModule; - } - - /** - * 获取页面列表 - */ - get pages() { - const ret: IPageConfigData[] = []; - this.routeModule?.routes.forEach((item) => { - if (item.path !== '*') { - ret.push({ - path: item.path, - name: item.component, - }); - } - }); - return ret; - } - - get bizComps(): string[] { - const packages = this.tangoConfigJson?.getValue('packages'); - let list = this.tangoConfigJson?.getValue('bizDependencies') || []; - if (packages) { - list = [ - ...new Set([ - ...list, - ...Object.keys(packages).filter((e) => packages[e].type === 'bizDependency'), - ]), - ]; - } - return list; - } - - get baseComps(): string[] { - const packages = this.tangoConfigJson?.getValue('packages'); - let list = this.tangoConfigJson?.getValue('baseDependencies') || []; - if (packages) { - list = [ - ...new Set([ - ...list, - ...Object.keys(packages).filter((e) => packages[e].type === 'baseDependency'), - ]), - ]; - } - return list; - } - - get localComps(): string[] { - return Object.keys(this.componentsEntryModule?.exportList || {}); - } - - get fileErrors() { - const errors: IFileError[] = []; - this.files.forEach((file) => { - if (file.isError) { - errors.push({ - filename: file.filename, - message: file.errorMessage, - }); - } - }); - return errors; - } - - constructor(options?: IWorkspaceOptions) { - super(); - this.history = new TangoHistory(this); - this.selectSource = new SelectSource(this); - this.dragSource = new DragSource(this); - this.componentPrototypes = new Map(); - this.entry = options?.entry; - this.activeRoute = options?.defaultActiveRoute || '/'; - this.activeFile = options?.entry; - this.activeViewFile = ''; - this.files = new Map(); - this.isReady = false; - - if (options?.onFilesChange) { - // 使用用户提供的 onFilesChange - this.onFilesChange = options.onFilesChange; - } - - if (options?.files) { - this.addFiles(options.files); - } - - if (options?.prototypes) { - this.setComponentPrototypes(options.prototypes); - } - +export class Workspace extends AbstractCodeWorkspace { + constructor(options?: IWorkspaceInitConfig) { + super(options); makeObservable(this, { files: observable, activeRoute: observable, @@ -275,1057 +25,7 @@ export class Workspace extends EventTarget implements IWorkspace { }); } - getPrototype(name: string | IComponentPrototype) { - if (isString(name)) { - return this.componentPrototypes.get(name); - } - return name as IComponentPrototype; - } - - /** - * 设置当前路由 - * @param routePath 路由路径 - */ - setActiveRoute(routePath: string) { - if (routePath === this.activeRoute) { - return; - } - this.selectSource.clear(); - this.activeRoute = routePath; - this.setActiveViewFile(routePath); - } - - /** - * 设置当前选中的文件 - * @param filename - */ - setActiveFile(filename: string, isViewFile = false) { - this.activeFile = filename; - if (isViewFile) { - this.activeViewFile = filename; - } - } - - /** - * 根据当前的路由计算当前的视图模块 - */ - setActiveViewFile(routePath: string) { - let filename = this.getFilenameByRoutePath(routePath); - if (!filename) { - // 没有找到 route 对应的文件,使用默认的 entry - for (const [key, file] of this.files) { - if (file.type === FileType.JsxViewModule) { - filename = file.filename; - break; - } - } - } - if (filename) { - this.setActiveFile(filename, true); - } - } - - setComponentPrototypes(prototypes: Record) { - Object.keys(prototypes).forEach((name) => { - this.componentPrototypes.set(name, prototypes[name]); - }); - } - - /** - * 添加一组文件到工作区,如果文件同名,后面的文件会覆盖前面的文件 - * @param files - */ - addFiles(files: IFileConfig[] = []) { - files.forEach((file) => { - this.addFile(file.filename, file.code, file.type); - }); - } - - /** - * 添加文件到工作区 - * @param filename 文件名 - * @param code 代码片段 - * @param fileType 模块类型 - */ addFile(filename: string, code: string, fileType?: FileType) { - if (!fileType && filename === this.entry) { - fileType = FileType.AppEntryModule; - } - const moduleType = fileType || inferFileType(filename); - const props = { - filename, - code, - type: moduleType, - }; - - let module; - switch (moduleType) { - case FileType.AppEntryModule: - module = new AppEntryModule(this, props); - this.appEntryModule = module; - break; - case FileType.StoreEntryModule: - module = new TangoStoreEntryModule(this, props); - this.storeEntryModule = module; - break; - case FileType.ComponentsEntryModule: - module = new TangoComponentsEntryModule(this, props); - this.componentsEntryModule = module; - break; - case FileType.RouteModule: { - module = new TangoRouteModule(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.JsxViewModule: - module = new TangoViewModule(this, props); - break; - case FileType.ServiceModule: - module = new TangoServiceModule(this, props); - this.serviceModules[module.name] = module; - break; - case FileType.StoreModule: - module = new TangoStoreModule(this, props); - this.storeModules[module.name] = module; - break; - case FileType.Module: - module = new TangoJsModule(this, props); - break; - case FileType.Less: - module = new TangoLessFile(this, props); - break; - case FileType.PackageJson: - module = new TangoJsonFile(this, props); - this.packageJson = module; - break; - case FileType.TangoConfigJson: - module = new TangoJsonFile(this, props); - this.tangoConfigJson = module; - break; - case FileType.Json: - module = new TangoJsonFile(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.ServiceModule); - 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(); - } - - /** - * 添加视图文件 - * @param viewName 文件名 - * @param code 代码 - */ - addViewFile(viewName: string, code: string) { - const viewRoute = viewName.startsWith('/') ? viewName : `/${viewName}`; - const filename = `/src/pages/${viewName}.js`; - this.addFile(filename, code); - this.addRoute( - { - name: viewName, - path: viewRoute, - }, - filename, - ); - } - - updateFile(filename: string, code: string, isSyncAst = true) { - const file = this.getFile(filename); - if (file instanceof TangoModule) { - file.update(code, isSyncAst); - } else { - file.update(code); - } - - const shouldFormat = this.projectConfig?.designerConfig?.autoFormatCode; - if (shouldFormat && file instanceof TangoViewModule) { - file.removeUnusedImportSpecifiers().update(); - } - this.history.push({ - message: HistoryMessage.UpdateCode, - data: { - [filename]: code, - }, - }); - } - - syncFiles() { - this.files.forEach((file) => { - if (file instanceof TangoModule) { - file.updateAst(); - } - }); - } - - removeFile(filename: string) { - if (this.files.get(filename)) { - // 如果是文件,直接删除 - this.files.delete(filename); - } else { - // 没有匹配到,就是一个目录,直接删除整个目录 - // FIXME: 可能存在风险,如果文件夹中的模块被复用,则会导致误删除 - Array.from(this.files.keys()).forEach((key) => { - if (key.startsWith(`${filename}/`)) { - this.files.delete(key); - } - }); - } - } - - /** - * 重命名文件 - * @param oldFilename - * @param newFilename - */ - renameFile(oldFilename: string, newFilename: string) { - const file = this.files.get(oldFilename); - if (file) { - this.removeFile(oldFilename); - this.addFile(newFilename, file.code); - } - } - - /** - * 重命名文件夹 - * @param oldFoldername 旧文件夹名 - * @param newFoldername 新文件夹名 - */ - renameFolder(oldFoldername: string, newFoldername: string) { - Array.from(this.files.keys()).forEach((key) => { - if (key.startsWith(`${oldFoldername}/`)) { - const newKey = key.replace(oldFoldername, newFoldername); - this.renameFile(key, newKey); - } - }); - } - - /** - * 根据文件名获取文件对象 - * @param filename - * @returns - */ - getFile(filename: string) { - return this.files.get(filename); - } - - /** - * 获取文件列表 - * @returns { [filename]: fileCode } - */ - listFiles() { - const ret: Dict = {}; - this.files.forEach((file) => { - ret[file.filename] = file.cleanCode; - }); - return ret; - } - - /** - * 删除视图模块 - * @param route 路由名称 - */ - removeViewModule(routePath: string) { - // get filename first - const filename = this.getFilenameByRoutePath(routePath); - if (this.routeModule) { - this.routeModule.removeRoute(routePath).update(); - this.setActiveRoute(this.routeModule.routes[0]?.path || '/'); - } - this.removeFile(filename); - } - - /** - * 添加新的路由 - */ - addRoute(routeData: IPageConfigData, importFilePath: string) { - this.routeModule?.addRoute(routeData.path, importFilePath).update(); - } - - /** - * 更新页面路由配置 - * @param sourceRoutePath - * @param targetPageData - */ - updateRoute(sourceRoutePath: string, targetPageData: IPageConfigData) { - if (sourceRoutePath !== targetPageData.path) { - this.routeModule?.updateRoute(sourceRoutePath, targetPageData.path).update(); - } - } - - /** - * 复制视图文件 - * @param sourceRoute - * @param targetRouteConfig - */ - copyViewPage(sourceRoutePath: string, targetPageData: IPageConfigData) { - const sourceFilePath = this.getRealViewFilePath(this.getFilenameByRoutePath(sourceRoutePath)); - const targetFilePath = getFilepath(targetPageData.path, '/src/pages'); - this.copyFiles(sourceFilePath, targetFilePath); - this.addRoute(targetPageData, targetFilePath); - } - - getNode(id: string, filename?: string) { - const file = filename ? this.getFile(filename) : this.activeViewModule; - if (file instanceof TangoViewModule) { - return file.getNode(id); - } - } - - /** - * 应用代码初始化完成 - */ - ready() { - if (!this.isReady) { - this.isReady = true; - - this.history.push({ - message: HistoryMessage.InitView, - data: { - [this.activeViewModule?.filename]: this.activeViewModule?.code, - }, - }); - } - } - - /** - * 添加新的模型文件 - * @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 = {}; - 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, 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(); - } - - addDependency(data: any) { - // TODO: implement it to replace addBizComp & addServiceComp - } - - /** - * 获取 package.json 中的依赖列表 - * @returns - * TODO: fix this logic to merge dependencies from package.json and tango.config.json - */ - listDependencies() { - return this.packageJson?.getValue('dependencies'); - } - - getDependency(pkgName: string) { - const packages = this.tangoConfigJson?.getValue('packages'); - const dependencies = this.packageJson?.getValue('dependencies'); // 兼容老版本 - const detail = { - version: dependencies?.[pkgName], - ...(packages?.[pkgName] || {}), - }; - return detail; - } - - /** - * 更新依赖,没有就添加 - * @param name - * @param version - * FIXME: 参数3的设置需要重新考虑下 - */ - updateDependency( - name: string, - version: string, - options?: { - package?: ITangoConfigPackages; - [x: string]: any; - }, - ) { - this.packageJson - ?.setValue('dependencies', (deps = {}) => { - deps[name] = version; - return deps; - }) - .update(); - - this.tangoConfigJson - ?.setValue('packages', (packages) => { - // 兼容以前的逻辑,只在拥有 package 参数时,才会更新 packages 字段 - if (!packages) { - return undefined; - } - - setValue(packages, name, { - ...packages[name], // 保留原有的配置 - version, // 更新版本号 - ...options?.package, // 更新 package 配置 - }); - - return packages; - }) - .update(); - - this.history.push({ - message: HistoryMessage.UpdateDependency, - data: { - [this.packageJson.filename]: this.packageJson.code, - }, - }); - } - - /** - * 移除依赖 - * @param name - */ - removeDependency(name: string) { - this.packageJson - ?.setValue('dependencies', (deps) => { - if (deps[name]) { - delete deps[name]; - } - return deps; - }) - .update(); - - this.tangoConfigJson - ?.setValue('packages', (packages = {}) => { - if (packages?.[name]) { - delete packages[name]; - } - - return packages; - }) - .update(); - - this.history.push({ - message: HistoryMessage.RemoveDependency, - data: { - [this.packageJson.filename]: this.packageJson.code, - }, - }); - } - - /** - * 删除业务组件 - * @param name - */ - removeBizComp(name: string) { - this.tangoConfigJson - ?.setValue('bizDependencies', (deps?: string[]) => { - if (!deps) { - return undefined; - } - return deps.filter((dep) => dep !== name); - }) - .update(); - this.removeDependency(name); - } - - /** - * 添加业务组件 - * @param name - */ - addBizComp( - name: string, - version: string, - options?: { - package?: ITangoConfigPackages; - [x: string]: any; - }, - ) { - const packages = this.tangoConfigJson.getValue('packages'); - this.updateDependency(name, version, { - ...options, - ...(!!packages && { - package: { - ...options?.package, - type: 'bizDependency', - }, - }), - }); - - // 兼容以前的逻辑 - if (!options?.package && !packages) { - // TODO: if tangoConfigJson not found, init this file - this.tangoConfigJson - ?.setValue('bizDependencies', (deps: string[] = []) => { - if (!deps.includes(name)) { - deps.push(name); - } - return deps; - }) - .update(); - } - - this.tangoConfigJson && - this.history.push({ - message: HistoryMessage.UpdateDependency, - data: { - [this.tangoConfigJson.filename]: this.tangoConfigJson.code, - }, - }); - } - - /** - * 删除选中节点 - */ - removeSelectedNode() { - const file = this.selectSource.file; - if (!file) return; - - // 选中的结点一定位于相同的文件中 - this.selectSource.nodes.forEach((node) => { - file.removeNode(node.id); - }); - file.update(); - this.selectSource.clear(); - this.history.push({ - message: HistoryMessage.RemoveNode, - data: { - [file.filename]: file.code, - }, - }); - } - - /** - * 复制选中结点 - */ - copySelectedNode() { - this.copyTempNodes = this.selectSource.nodes as TangoNode[]; - } - - /** - * 粘贴选中结点 - * @deprecated 考虑废弃 - * TODO: 重构该逻辑,抽离出公共的方法 - */ - pasteSelectedNode() { - if (this.selectSource.size !== 1) return; - if (!this.copyTempNodes) return; - - // TODO: 潜在隐患,如果跨页的话,代码里的逻辑调用也要处理 - - const importDeclarations = this.getImportDeclarationByNodes( - this.copyTempNodes.map((node) => node.rawNode), - ); - - importDeclarations.forEach((importDeclaration) => { - this.activeViewModule.updateImportSpecifiersLegacy(importDeclaration); - }); - - this.copyTempNodes.forEach((node) => { - this.activeViewModule.insertAfter(this.selectSource.first.id, node.cloneRawNode()); - }); - - this.activeViewModule.update(); - - this.history.push({ - message: HistoryMessage.CloneNode, - data: { - [this.activeViewModule.filename]: this.activeViewModule.code, - }, - }); - } - - /** - * 克隆选中节点,追加到当前结点的后方 - */ - cloneSelectedNode() { - const file = this.selectSource.file; - - let injectedProps; - if (this.projectConfig?.designerConfig?.autoGenerateComponentId) { - // 生成新的组件 id - injectedProps = { - tid: file.idGenerator.generateId(this.selectSource.firstNode.component).id, - }; - } - - file - .insertAfter( - this.selectSource.first.id, - this.selectSource.firstNode.cloneRawNode(injectedProps) as JSXElement, - ) - .update(); - - this.history.push({ - message: HistoryMessage.CloneNode, - data: { - [file.filename]: file.code, - }, - }); - } - - /** - * 在目标节点中插入子节点 - * @param targetNodeId 目标节点dnd-id - * @param sourceName 插入的组件名称 - * @returns - */ - insertToNode(targetNodeId: string, sourceName: string | IComponentPrototype) { - if (!targetNodeId || !sourceName) { - return; - } - - const sourcePrototype = this.getPrototype(sourceName); - const newNode = prototype2jsxElement(sourcePrototype); - const file = this.getNode(targetNodeId).file; - const { source, specifiers } = prototype2importDeclarationData(sourcePrototype, file.filename); - file - .insertChild(targetNodeId, newNode, 'last') - .addImportSpecifiers(source, specifiers) - .update(); - this.history.push({ - message: HistoryMessage.InsertNode, - data: { - [file.filename]: file.code, - }, - }); - } - - /** - * 替换目标节点 - */ - replaceNode(targetNodeId: string, sourceName: string | IComponentPrototype) { - if (!targetNodeId || !sourceName) { - return; - } - - const sourcePrototype = this.getPrototype(sourceName); - const newNode = prototype2jsxElement(sourcePrototype); - const file = this.getNode(targetNodeId).file; - const { source, specifiers } = prototype2importDeclarationData(sourcePrototype, file.filename); - file.replaceNode(targetNodeId, newNode).addImportSpecifiers(source, specifiers).update(); - this.history.push({ - message: HistoryMessage.ReplaceNode, - data: { - [file.filename]: file.code, - }, - }); - } - - /** - * 在选中节点中插入子节点 - * @param childName 节点名 - */ - insertToSelectedNode(childName: string | IComponentPrototype) { - const insertedPrototype = this.getPrototype(childName); - if (insertedPrototype) { - const newNode = prototype2jsxElement(insertedPrototype); - const file = this.selectSource.file; - const { source, specifiers } = prototype2importDeclarationData( - insertedPrototype, - file.filename, - ); - file - .insertChild(this.selectSource.first.id, newNode, 'last') - .addImportSpecifiers(source, specifiers) - .update(); - this.history.push({ - message: HistoryMessage.InsertNode, - data: { - [file.filename]: file.code, - }, - }); - } - } - - insertBeforeSelectedNode(sourceName: string | IComponentPrototype) { - if (!sourceName) { - return; - } - const targetNodeId = this.selectSource.first.id; - const sourcePrototype = this.getPrototype(sourceName); - const newNode = prototype2jsxElement(sourcePrototype); - const file = this.getNode(targetNodeId).file; - const { source, specifiers } = prototype2importDeclarationData(sourcePrototype, file.filename); - - file.insertBefore(targetNodeId, newNode).addImportSpecifiers(source, specifiers).update(); - - this.history.push({ - message: HistoryMessage.InsertBeforeNode, - data: { - [file.filename]: file.code, - }, - }); - } - - insertAfterSelectedNode(sourceName: string | IComponentPrototype) { - if (!sourceName) { - return; - } - const targetNodeId = this.selectSource.first.id; - const sourcePrototype = this.getPrototype(sourceName); - const newNode = prototype2jsxElement(sourcePrototype); - const file = this.getNode(targetNodeId).file; - const { source, specifiers } = prototype2importDeclarationData(sourcePrototype, file.filename); - - file.insertAfter(targetNodeId, newNode).addImportSpecifiers(source, specifiers).update(); - - this.history.push({ - message: HistoryMessage.InsertAfterNode, - data: { - [file.filename]: file.code, - }, - }); - } - - updateSelectedNodeAttributes( - attributes: Record = {}, - relatedImports: string[] = [], - ) { - const file = this.selectSource.file; - file.updateNodeAttributes(this.selectSource.first.id, attributes, relatedImports).update(); - this.history.push({ - message: HistoryMessage.UpdateAttribute, - data: { - [file.filename]: file.code, - }, - }); - } - - /** - * 将节点拽入视图中 - */ - dropNode() { - const dragSource = this.dragSource; - const dropTarget = dragSource.dropTarget; - - if (!dragSource.prototype || !dropTarget.id) { - // 无效的 drag source 或 drop target,提前退出 - logger.error('invalid dragSource or dropTarget'); - return; - } - - // TODO: 这里需要一个额外的信息,DropTarget 的最近容器节点,用于判断目标元素是否可以被置入容器中 - const dragSourcePrototype = dragSource.prototype; - - let injectedProps; // 额外注入给节点的属性 - if (this.projectConfig?.designerConfig?.autoGenerateComponentId) { - injectedProps = { - tid: this.activeViewModule.idGenerator.generateId(dragSource.prototype.name).id, - }; - } - - let newNode; - if (dragSource.id) { - // 来自画布,完整的克隆该节点 - newNode = dragSource.getNode().cloneRawNode(); - } else { - // 来自物料面板,创建新的初始化节点 - newNode = prototype2jsxElement(dragSource.prototype, injectedProps); - } - - if (!newNode) { - return; - } - - const targetFile = dropTarget.node?.file; - const sourceFile = dragSource.node?.file; - - // dragSourcePrototype to importDeclarations - const { source, specifiers } = prototype2importDeclarationData( - dragSourcePrototype, - targetFile.filename, - ); - - let isValidOperation = true; - switch (dropTarget.method) { - // 直接往目标节点的 children 里添加一个节点 - case DropMethod.InsertChild: { - targetFile - .insertChild(dropTarget.id, newNode, 'last') - .addImportSpecifiers(source, specifiers); - break; - } - case DropMethod.InsertFirstChild: { - targetFile - .insertChild(dropTarget.id, newNode, 'first') - .addImportSpecifiers(source, specifiers); - break; - } - // 往目标节点的后边插入一个节点 - case DropMethod.InsertAfter: { - targetFile.insertAfter(dropTarget.id, newNode).addImportSpecifiers(source, specifiers); - break; - } - // 往目标节点的前方插入一个节点 - case DropMethod.InsertBefore: { - targetFile.insertBefore(dropTarget.id, newNode).addImportSpecifiers(source, specifiers); - break; - } - // 替换目标节点 - case DropMethod.ReplaceNode: { - targetFile.replaceNode(dropTarget.id, newNode).addImportSpecifiers(source, specifiers); - break; - } - default: - isValidOperation = false; - break; - } - - // 如果拖拽来源有 ID,表示来自画布 - const isDraggingFromView = !!dragSource.id; - - if (isValidOperation) { - if (isDraggingFromView) { - sourceFile.removeNode(dragSource.id); - } - - this.selectSource.clear(); - } - - targetFile.update(); - if (isDraggingFromView && sourceFile.filename !== targetFile.filename) { - sourceFile.update(); - } - - dragSource.clear(); - - if (isValidOperation) { - this.history.push({ - message: HistoryMessage.DropNode, - data: { - [targetFile.filename]: targetFile.code, - }, - }); - } - } - - onFilesChange(filenams: string[]) { - // do nothing - } - - /** - * 刷新目标文件 - * @param filenames - */ - refresh(filenames: string[]) { - this.dispatchEvent( - new CustomEvent('refresh', { - detail: { - filenames, - entry: this.entry, - }, - }), - ); - } - - /** - * 基于输入结点获得结点依赖的导入声明信息 - * @param nodes - */ - private getImportDeclarationByNodes(nodes: JSXElement[]) { - let names = nodes.reduce((prev, cur) => { - prev = prev.concat(getJSXElementChildrenNames(cur)); - return prev; - }, []); - names = uniq(names); - const importDeclarations = namesToImportDeclarations(names, this.selectSource.file.importMap); - return importDeclarations; - } - - /** - * 根据路由路径获取文件名 - * @param routePath - * @returns - */ - private getFilenameByRoutePath(routePath: string) { - let filename: string; - this.routeModule?.routes.forEach((route) => { - if (isPathnameMatchRoute(routePath, route.path) && route.importPath) { - if (route.importPath.startsWith('@/')) { - filename = route.importPath; - const alias = this.tangoConfigJson.getValue('sandbox.alias') || {}; - if (alias['@']) { - filename = filename.replace('@', alias['@']); - } - filename = this.getRealViewFilePath(filename); - } else { - const absolutePath = route.importPath.replace('.', '/src'); - filename = this.getRealViewFilePath(absolutePath); - } - } - }); - return filename; - } - - private getRealViewFilePath(filePath: string): string { - // 如果有后缀名直接返回 - if (hasFileExtension(filePath)) { - return filePath; - } - - const possiblePaths = [ - `${filePath}.js`, - `${filePath}.jsx`, - `${filePath}/index.js`, - `${filePath}/index.jsx`, - ]; - - for (const filepath of possiblePaths) { - if (this.files.has(filepath)) { - return filepath; - } - } - } - - /** - * 文件拷贝 - * @param sourcePath - * @param targetPath - */ - private copyFiles(sourceFilePath: string, targetFilePath: string) { - if (this.files.has(sourceFilePath)) { - // 来源是文件 - const file = this.files.get(sourceFilePath); - this.addFile(`${targetFilePath}.js`, file.cleanCode, file.type); - } else if (this.files.has(`${sourceFilePath}/index.js`)) { - // 来源是目录 - Array.from(this.files.keys()).forEach((key) => { - if (key.startsWith(`${sourceFilePath}/`)) { - const sourceFile = this.getFile(key); - this.addFile( - targetFilePath + key.slice(sourceFilePath.length), - sourceFile.cleanCode, - sourceFile.type, - ); - } - }); - } else { - logger.error('copyFiles failed, source: %s, target: %s', sourceFilePath, targetFilePath); - } + super.addFile(filename, code, fileType); } } diff --git a/packages/core/src/types.ts b/packages/core/src/types.ts index 94da69a4..a61692e5 100644 --- a/packages/core/src/types.ts +++ b/packages/core/src/types.ts @@ -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 { +export interface IViewNodeData { /** * 节点 ID */ @@ -97,7 +85,7 @@ export interface ITangoViewNodeData { /** * 子节点列表 */ - children?: Array>; + children?: Array>; } /** diff --git a/packages/core/tests/helpers.test.ts b/packages/core/tests/helpers.test.ts index 2a1405ed..e1ba7618 100644 --- a/packages/core/tests/helpers.test.ts +++ b/packages/core/tests/helpers.test.ts @@ -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); }); }); diff --git a/packages/designer/src/dnd/use-dnd.ts b/packages/designer/src/dnd/use-dnd.ts index b7802ce8..71ef5b80 100644 --- a/packages/designer/src/dnd/use-dnd.ts +++ b/packages/designer/src/dnd/use-dnd.ts @@ -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; } diff --git a/packages/designer/src/sandbox/sandbox.tsx b/packages/designer/src/sandbox/sandbox.tsx index b918b568..ba97a897 100644 --- a/packages/designer/src/sandbox/sandbox.tsx +++ b/packages/designer/src/sandbox/sandbox.tsx @@ -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 ? { diff --git a/packages/designer/src/setters/code-setter.tsx b/packages/designer/src/setters/code-setter.tsx index d848fcb5..e6367536 100644 --- a/packages/designer/src/setters/code-setter.tsx +++ b/packages/designer/src/setters/code-setter.tsx @@ -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) => { diff --git a/packages/designer/src/sidebar/dependency-panel.tsx b/packages/designer/src/sidebar/dependency-panel.tsx index e52fb626..3b56dc3b 100644 --- a/packages/designer/src/sidebar/dependency-panel.tsx +++ b/packages/designer/src/sidebar/dependency-panel.tsx @@ -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; diff --git a/packages/designer/src/sidebar/outline-panel/components-tree.tsx b/packages/designer/src/sidebar/outline-panel/components-tree.tsx index 3127fce2..133666cf 100644 --- a/packages/designer/src/sidebar/outline-panel/components-tree.tsx +++ b/packages/designer/src/sidebar/outline-panel/components-tree.tsx @@ -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 = 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);