diff --git a/.prettierignore b/.prettierignore index 2f118daef..339277e41 100644 --- a/.prettierignore +++ b/.prettierignore @@ -1,3 +1,5 @@ dist package-lock.json -**/node_modules/** \ No newline at end of file +**/node_modules/** +# 忽略该文件夹下的测试对比文件,防止自动去掉分号之后导致测试失败 +packages/build/vite-plugin-meta-comments/test/expected/** \ No newline at end of file diff --git a/designer-demo/package.json b/designer-demo/package.json index b3bf27d9e..13a8bd99b 100644 --- a/designer-demo/package.json +++ b/designer-demo/package.json @@ -12,6 +12,7 @@ }, "dependencies": { "@opentiny/tiny-engine": "workspace:^", + "@opentiny/tiny-engine-meta-register": "workspace:^", "@opentiny/tiny-engine-utils": "workspace:*", "@opentiny/vue": "~3.20.0", "@opentiny/vue-design-smb": "~3.20.0", diff --git a/designer-demo/registry.example b/designer-demo/registry.example new file mode 100644 index 000000000..0001d73f0 --- /dev/null +++ b/designer-demo/registry.example @@ -0,0 +1,62 @@ +/** + * Copyright (c) 2023 - present TinyEngine Authors. + * Copyright (c) 2023 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ +// 注册表示例 +import { META_SERVICE, META_APP } from '@opentiny/tiny-engine-meta-register' +import engineConfig from './engine.config' +import { HttpService } from './src/composable' +import scriptPlugin from './src/plugins/script' + +export default { + [META_SERVICE.Http]: HttpService, + 'engine.config': { + ...engineConfig + }, + // 配置 false 隐藏工具栏清空按钮 + [META_APP.Clean]: false, + // 配置 false 隐藏大纲树,手动配置 tree-shaking 为 false,仍然不会被 tree-shaking + // #__TINY_ENGINE_TREE_SHAKING__: false + [META_APP.OutlineTree]: false, + // 替换整个页面JS插件,手动配置 tree-shaking 为 true + /* #__TINY_ENGINE_TREE_SHAKING__: true */ + [META_APP.Page]: scriptPlugin, + // 新增模块 + 'engine.plugins.custom_id': { + ...cuttomPlugin, + id: 'engine.plugins.custom_id' + }, + // 调整插件顺序 + [META_APP.Layout]: { + options: { + relativeLayoutConfig: { + [META_APP.Script]: { + insertBefore: META_APP.AppManage + }, + // 调整插件顺序 + [META_APP.Materials]: { + insertAfter: META_APP.State + }, + // 调整插件上下位置 + [META_APP.Schema]: { + insertBefore: META_APP.Materials + }, + // 调整工具栏顺序 + [META_APP.Save]: { + insertBefore: META_APP.ThemeSwitch + }, + // 支持切换组 + [META_APP.Lang]: { + insertAfter: META_APP.Breadcrumb + } + } + } + } +} diff --git a/designer-demo/registry.js b/designer-demo/registry.js index 032a8a968..23d7d0c82 100644 --- a/designer-demo/registry.js +++ b/designer-demo/registry.js @@ -1,6 +1,6 @@ /** - * Copyright (c) 2024 - present TinyEngine Authors. - * Copyright (c) 2024 - present Huawei Cloud Computing Technologies Co., Ltd. + * Copyright (c) 2023 - present TinyEngine Authors. + * Copyright (c) 2023 - present Huawei Cloud Computing Technologies Co., Ltd. * * Use of this source code is governed by an MIT-style license. * @@ -9,116 +9,39 @@ * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. * */ - -import { - Breadcrumb, - Fullscreen, - Lang, - ViewSetting, - Logo, - Lock, - Media, - Redoundo, - Save, - Clean, - ThemeSwitch, - Preview, - GenerateCode, - Refresh, - Collaboration, - Materials, - State, - Script, - Tree, - Help, - Schema, - Page, - I18n, - Bridge, - Block, - Datasource, - Robot, - Props, - Events, - Styles, - Layout, - Canvas, - GenerateCodeService, - GlobalService, - ThemeSwitchService -} from '@opentiny/tiny-engine' +import { META_SERVICE, META_APP } from '@opentiny/tiny-engine-meta-register' import engineConfig from './engine.config' import { HttpService } from './src/composable' export default { - root: { - id: 'engine.root', - metas: [HttpService, GenerateCodeService, GlobalService, ThemeSwitchService] // GlobalService 依赖 HttpService,HttpService需要在前面处理 + [META_SERVICE.Http]: HttpService, + 'engine.config': { + ...engineConfig }, - config: engineConfig, - layout: { - ...Layout, + // 调整插件顺序示例: + [META_APP.Layout]: { options: { - ...Layout.options, - isShowLine: true, - isShowCollapse: true, - toolbars: { - left: ['engine.toolbars.breadcrumb', 'engine.toolbars.lock', 'engine.toolbars.logo'], - center: ['engine.toolbars.media'], - right: [ - ['engine.toolbars.themeSwitch', 'engine.toolbars.redoundo', 'engine.toolbars.clean'], - ['engine.toolbars.preview'], - ['engine.toolbars.generate-code', 'engine.toolbars.save'] - ], - collapse: [ - ['engine.toolbars.collaboration'], - ['engine.toolbars.refresh', 'engine.toolbars.fullscreen'], - ['engine.toolbars.lang'], - ['engine.toolbars.viewSetting'] - ] + relativeLayoutConfig: { + [META_APP.Page]: { + insertBefore: META_APP.State + }, + // 调整插件顺序 + [META_APP.OutlineTree]: { + insertAfter: META_APP.Materials + }, + // 调整插件上下位置 + [META_APP.Schema]: { + insertBefore: META_APP.Help + }, + // 调整工具栏顺序 + [META_APP.Save]: { + insertAfter: META_APP.GenerateCode + }, + // 支持切换组 + [META_APP.Lang]: { + insertAfter: META_APP.ViewSetting + } } } - }, - themes: [ - { - id: 'engine.theme.light' - }, - { - id: 'engine.theme.dark' - } - ], - toolbars: [ - ThemeSwitch, - Logo, - Breadcrumb, - Lock, - Media, - Redoundo, - Collaboration, - Clean, - Preview, - Refresh, - GenerateCode, - Save, - Fullscreen, - Lang, - ViewSetting - ], - plugins: [ - Materials, - Tree, - Page, - [Block, { options: { ...Block.options, mergeCategoriesAndGroups: true } }], - Datasource, - Bridge, - I18n, - Script, - State, - Schema, - Help, - Robot - ], - dsls: [{ id: 'engine.dsls.dslvue' }], - settings: [Props, Styles, Events], - canvas: Canvas + } } diff --git a/designer-demo/src/defineEntry.js b/designer-demo/src/defineEntry.js deleted file mode 100644 index f8b215c1b..000000000 --- a/designer-demo/src/defineEntry.js +++ /dev/null @@ -1,18 +0,0 @@ -/** - * Copyright (c) 2023 - present TinyEngine Authors. - * Copyright (c) 2023 - present Huawei Cloud Computing Technologies Co., Ltd. - * - * Use of this source code is governed by an MIT-style license. - * - * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, - * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR - * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. - * - */ - -import registry from '../registry.js' -import { defineEntry } from '@opentiny/tiny-engine' - -defineEntry(registry) - -export { registry } diff --git a/designer-demo/src/main.js b/designer-demo/src/main.js index 10457cd95..be2226887 100644 --- a/designer-demo/src/main.js +++ b/designer-demo/src/main.js @@ -9,15 +9,19 @@ * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. * */ - -// 导入@opentiny/tiny-engine时,内部的依赖包也会逐个导入,可能会执行useComplie,此时需要templateHashMap。所以需要先执行一次defineEntry -import { registry } from './defineEntry.js' -import { init } from '@opentiny/tiny-engine' import { configurators } from './configurators/' import 'virtual:svg-icons-register' -init({ - registry, - configurators, - createAppSignal: ['global_service_init_finish'] -}) +async function startApp() { + const registry = await import('../registry') + const { init } = await import('@opentiny/tiny-engine') + + init({ + // 合并多个注册表 + registry: [registry.default], + configurators, + createAppSignal: ['global_service_init_finish'] + }) +} + +startApp() diff --git a/designer-demo/src/preview.js b/designer-demo/src/preview.js index ea23c3756..4c38a8591 100644 --- a/designer-demo/src/preview.js +++ b/designer-demo/src/preview.js @@ -9,30 +9,34 @@ * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. * */ - -import { initHook, HOOK_NAME, GenerateCodeService, Breadcrumb, Media, Lang } from '@opentiny/tiny-engine' -import { initPreview } from '@opentiny/tiny-engine' +import { defineEntry } from '@opentiny/tiny-engine-meta-register' import 'virtual:svg-icons-register' -import { HttpService } from './composable' -const beforeAppCreate = () => { - initHook(HOOK_NAME.useEnv, import.meta.env) -} +async function startApp () { + const { initHook, HOOK_NAME, META_SERVICE, initPreview } = await import('@opentiny/tiny-engine') + const { HttpService } = await import('./composable') -initPreview({ - registry: { - root: { - id: 'engine.root', - metas: [HttpService, GenerateCodeService] - }, - config: { + const beforeAppCreate = () => { + initHook(HOOK_NAME.useEnv, import.meta.env) + } + + const registry = { + [META_SERVICE.Http]: HttpService, + 'engine.config': { id: 'engine.config', theme: 'light', - material: ['/mock/bundle.json'], - }, - toolbars: [Breadcrumb, Media, Lang] - }, - lifeCycles: { - beforeAppCreate + material: ['/mock/bundle.json'] + } } -}) + + defineEntry(registry) + + initPreview({ + registry, + lifeCycles: { + beforeAppCreate + } + }) +} + +startApp() diff --git a/designer-demo/vite.config.js b/designer-demo/vite.config.js index e60881797..b61244c64 100644 --- a/designer-demo/vite.config.js +++ b/designer-demo/vite.config.js @@ -8,7 +8,8 @@ export default defineConfig((configEnv) => { root: __dirname, iconDirs: [path.resolve(__dirname, './node_modules/@opentiny/tiny-engine/assets/')], useSourceAlias: true, - envDir: './env' + envDir: './env', + registryPath: './registry.js' }) const customConfig = { diff --git a/docs/README.md b/docs/README.md index 032c7fbb5..dd66df819 100644 --- a/docs/README.md +++ b/docs/README.md @@ -29,6 +29,13 @@ - [数据源和Collection—远程字段](./advanced-features/data-source-and-collection-remote-fields.md) - [数据源和Collection—mock数据](./advanced-features/data-source-and-collection-mock-data.md) - [数据源和Collection—使用数据源](./advanced-features/data-source-and-collection-usage.md) + - 路由能力 + - [页面支持嵌套路由](./advanced-features/route-capabilities/page-support-nested-route.md) + - [路由bar一键清除预览页面路径](./advanced-features/route-capabilities/route-bar-clear-preview-page.md) + - [路由bar高亮显示预览页面路径](./advanced-features/route-capabilities/route-bar-current-page-highlight.md) + - [RouterView组件支持预览子界面](./advanced-features/route-capabilities/route-view-support-preview-subpage.md) + - [主题切换功能](./advanced-features/theme-switch.md) + - [画布快捷操作](./advanced-features/canvas-shortcuts.md) - 教程 - [从零搭建一个页面](./tutorials/build-a-page-from-scratch.md) - [第一期2023.10.27](./tutorials/issue-1-2023.10.27.md) @@ -39,6 +46,9 @@ - [简介](./development-getting-started/dev-intro.md) - [快速上手](./development-getting-started/dev-quick-start.md) - [前后端启动联调(Java服务端)](./development-getting-started/debugging-of-java-backend.md) +- 更新日志 + - [更新日志](./changelog/changelog.md) + - [v2.6升级指南](./changelog/v2.6-upgrade-guide.md) - 解决方案 - [Java服务端部署](./solutions/server-deployment-solution-java.md) - [Node.js服务端部署](./solutions/server-deployment-solution.md) @@ -47,9 +57,13 @@ - [设计器中引入第三方组件库](./solutions/third-party-library-in-designer.md) - [物料同步方案](./solutions/material-sync-solution.md) - [本地化CDN方案](./solutions/import-map-local.md) + - [全新区块构建方案](./solutions/block-construction-solution.md) + - [全新画布通信方案](./solutions/canvas-communication-solution.md) - 扩展能力介绍 - [新架构介绍](./extension-capabilities-overview/new-architecture.md) - [注册表](./extension-capabilities-overview/registry.md) + - [注册表(新版)](./extension-capabilities-overview/new-registry.md) + - [注册表高级特性](./extension-capabilities-overview/new-registry-advanced.md) - [元服务和元应用](./extension-capabilities-overview/meta-services-and-meta-apps.md) - 扩展能力使用教程 - [如何开发插件](./extension-capabilities-tutorial/how-to-develop-plugins.md) @@ -63,6 +77,7 @@ - [定制元服务逻辑](./extension-capabilities-tutorial/customize-meta-service-logic.md) - [开发设置器组件](./extension-capabilities-tutorial/develop-configurator-components.md) - [AI插件使用前配置](./extension-capabilities-tutorial/ai-plugin-configuration.md) + - [如何自定义主题](./extension-capabilities-tutorial/how-to-custom-theme.md) - API - [API总览](./api/api-overview.md) - 前端API diff --git a/docs/api/frontend-api/global-layout-api.md b/docs/api/frontend-api/global-layout-api.md index bfba2336f..cc274686c 100644 --- a/docs/api/frontend-api/global-layout-api.md +++ b/docs/api/frontend-api/global-layout-api.md @@ -1,39 +1,322 @@ -# 布局模块 +# Layout 插件 -## 布局元应用 +## layout 插件 -布局元应用配置如下 +Layout 插件是 Tiny Engine 的核心布局组件,它定义了整个设计器的界面结构。主要包含以下部分: + +- 顶部工具栏(Toolbars) +- 左侧插件(Plugins) +- 右侧设置插件(Settings) +- 中间内容区域(Canvas) + +![设计器布局示意图](./imgs/platformExpend.png) + +### 使用方法 + +#### 注册表中修改配置 + +如果我们要修改 layout 插件的配置,我们可以在注册表 `registry.js` 文件中修改配置。 ```javascript -{ - options: { - configProvider, // 全局配置组件 - configProviderDesign // 规范,可以通过该属于定制一些自定义的交互规范 - }, - metas: [LayoutService] -} -``` - -## 布局元服务 - -布局元服务api如下 - -```javascript -{ - apis: { - PLUGIN_NAME, // 对象,插件对应的元应用id - activePlugin, // 激活plugin面板,参数(插件名,是否激活对应面板) - activeSetting, // 激活setting面板并高亮提示,参数(属性名) - closePlugin, // 关闭插件面板,参数(是否强制关闭) - getDimension, // 获取样式范围 - getPluginState, // 获取插件面板状态 - getScale, // 获取规定比例 - isEmptyPage, // 判断页面状态是否为空 - layoutState, // 对象,layout状态 - pluginState, // 对象,插件状态 - setDimension // 设置样式范围 +export default { + [META_APP.Layout]: { + options: { + // 局部调整插件顺序 + relativeLayoutConfig: {} + // ... laout 插件配置项 + } } } ``` -### \ No newline at end of file +支持的 options 配置项为: +- configProvider 全局配置组件 +- configProviderDesign 规范,可以通过该配置项定制一些自定义的交互规范 +- isShowLine 工具栏是否显示分隔线 +- isShowCollapse 工具栏是否显示折叠菜单 +- layoutConfig 完整布局配置,一旦配置此项,relativeLayoutConfig 将失效 +- relativeLayoutConfig 局部调整插件顺序,仅当 layoutConfig 未配置时生效 + +#### 自定义插件位置 + +自定义插件位置可以使用 options.layoutConfig 配置项,也可以使用 options.relativeLayoutConfig 配置项。 +⚠️注意:当使用 `options.relativeLayoutConfig` 配置项时,`options.layoutConfig` 配置项将失效。 + +##### 使用 options.relativeLayoutConfig 配置项修改插件位置 + +options.relativeLayoutConfig 可以局部调整插件位置。 +适用场景:仅调整局部的插件位置,或者是增加新插件时,指定顺序。 + +支持的相对位置有: +- insertBefore 显示在指定的插件前面 +- insertAfter 显示在指定的插件后面 + +使用示例: + +```javascript +export default { + [META_APP.Layout]: { + options: { + // 局部调整插件顺序 + relativeLayoutConfig: { + // 将页面 JS 插件显示在页面管理插件前面 + [META_APP.Script]: { + insertBefore: META_APP.AppManage + }, + // 将物料面板显示在状态管理插件后面 + [META_APP.Materials]: { + insertAfter: META_APP.State + }, + // 将页面 schema 插件显示在物料插件前面 + [META_APP.Schema]: { + insertBefore: META_APP.Materials + }, + // 将保存按钮显示在主题切换按钮前面 + [META_APP.Save]: { + insertBefore: META_APP.ThemeSwitch + }, + // 将自定义插件显示在大纲树后面 + 'engine.plugins.customPlugin': { + insertAfter: META_APP.OutlineTree + } + } + } + } +} +``` + +##### 使用 options.layoutConfig 配置项修改插件位置 + +options.layoutConfig 可以全局调整插件位置。 +适用场景:调整全局的插件、工具栏的位置,包括新增的插件。 +⚠️注意: +1. 使用 options.layoutConfig 配置项时,options.relativeLayoutConfig 配置项将失效 +2. 使用该配置项时,需要列举完整的插件列表。没有列举的插件,将不会显示。 + +默认的布局配置,请参考 [默认布局配置](https://github.com/opentiny/tiny-engine/blob/develop/packages/layout/src/defaultLayout.js)。 + +使用示例: + +```javascript +export default { + [META_APP.Layout]: { + options: { + layoutConfig: { + plugins: { + left: { + top: [ + META_APP.Materials, + META_APP.OutlineTree, + META_APP.AppManage, + META_APP.BlockManage, + META_APP.Collections, + META_APP.Bridge, + META_APP.I18n, + META_APP.PageController, + META_APP.State + ], + bottom: [META_APP.Schema, META_APP.EditorHelp, META_APP.Robot] + }, + right: { + top: [META_APP.Props, META_APP.Styles, META_APP.Event] + } + }, + toolbars: { + left: [META_APP.Breadcrumb, META_APP.Lock, META_APP.Logo], + center: [META_APP.Media], + right: [ + [META_APP.ThemeSwitch, META_APP.RedoUndo, META_APP.Clean], + [META_APP.Preview], + [META_APP.GenerateCode, META_APP.Save] + ], + collapse: [ + [META_APP.Collaboration], + [META_APP.Refresh, META_APP.Fullscreen], + [META_APP.Lang], + [META_APP.ViewSetting] + ] + } + } + } + } +} +``` + +#### isShowLine 是否显示工具栏分隔线 + +isShowLine 配置项用于控制工具栏是否显示分隔线。 + +使用示例: + +```javascript +export default { + [META_APP.Layout]: { + options: { + isShowLine: false + } + } +} +``` + +显示分隔线示意图: + +![显示分隔线示意图](./imgs/showLineTrue.png) + +不显示分隔线示意图: + +![不显示分隔线示意图](./imgs/showLineFalse.png) + +#### isShowCollapse 是否显示工具栏折叠菜单 + +isShowCollapse 配置项用于控制工具栏是否显示折叠菜单。 + +使用示例: + +```javascript +export default { + [META_APP.Layout]: { + options: { + isShowCollapse: true + } + } +} +``` + +显示折叠菜单示意图: + +![显示折叠菜单示意图](./imgs/showCollapseTrue.png) + +不显示折叠菜单示意图: + +![不显示折叠菜单示意图](./imgs/showCollapseFalse.png) + +#### configProvider 全局配置组件 + +等同于 @opentiny/vue 的 [ConfigProvider](https://opentiny.design/tiny-vue/zh-CN/os-theme/components/config-provider#demos) 全局配置。 + +使用示例: + +```javascript +import { ConfigProvider as TinyConfigProvider } from '@opentiny/vue' + +export default { + [META_APP.Layout]: { + options: { + configProvider: TinyConfigProvider + } + } +} +``` + +#### configProviderDesign 规范 + +configProviderDesign 配置项用于配置规范,相当于给 ConfigProvider 组件的 design 配置项。 + +[ConfigProvider 组件文档](https://opentiny.design/tiny-vue/zh-CN/os-theme/components/config-provider#demos) + +使用示例: + +```javascript +import designSmbConfig from '@opentiny/vue-design-smb' + +export default { + [META_APP.Layout]: { + options: { + configProviderDesign: designSmbConfig + } + } +} +``` + +## useLayout 元服务 + +`useLayout` 是一个元服务,提供了管理布局状态和与布局交互的功能。 + +### 基本用法 + +```javascript +import { useLayout } from '@opentiny/tiny-engine' + +export default { + setup() { + const { + layoutState, + closePlugin, + closeSetting + } = useLayout() + + // 使用布局服务提供的方法和状态 + + return { + // ... + } + } +} +``` + +### API 参考 + +#### 状态 + +| 状态名称 | 类型 | 描述 | +|---------|------|------| +| layoutState | Object | 布局状态对象,包含 plugins 和 settings 状态 | +| leftFixedPanelsStorage | Ref\\> | 固定在左侧的面板列表 | +| rightFixedPanelsStorage | Ref\\> | 固定在右侧的面板列表 | + +#### 方法 + +| 方法名 | 参数 | 返回值 | 描述 | +|-------|------|-------|------| +| closePlugin | (forceClose?: boolean) | void | 关闭左侧插件面板 | +| closeSetting | (forceClose?: boolean) | void | 关闭右侧设置面板 | +| activePlugin | (pluginName: string) | Promise\ | 激活左侧插件面板 | +| activeSetting | (pluginName: string) | void | 激活右侧设置面板 | +| changeLeftFixedPanels | (pluginName: string) | void | 更改左侧固定面板 | +| changeRightFixedPanels | (pluginName: string) | void | 更改右侧固定面板 | + +### 示例 + +#### 切换插件面板 + +```javascript +import { useLayout, META_APP } from '@opentiny/tiny-engine' + +export default { + setup() { + const { activePlugin, activeSetting } = useLayout() + + // 打开材料面板 + const openMaterialsPanel = () => { + activePlugin(META_APP.Materials) + } + + // 打开属性设置面板 + const openPropsPanel = () => { + activeSetting(META_APP.Props) + } + } +} +``` + +#### 固定面板 + +```javascript +import { useLayout, META_APP } from '@opentiny/tiny-engine' + +export default { + setup() { + const { changeLeftFixedPanels, leftFixedPanelsStorage } = useLayout() + + // 固定或取消固定材料面板 + const toggleFixMaterialsPanel = () => { + changeLeftFixedPanels(META_APP.Materials) + } + + return { + toggleFixMaterialsPanel, + leftFixedPanelsStorage + } + } +} +``` diff --git a/docs/api/frontend-api/imgs/platformExpend.png b/docs/api/frontend-api/imgs/platformExpend.png new file mode 100644 index 000000000..26bf82749 Binary files /dev/null and b/docs/api/frontend-api/imgs/platformExpend.png differ diff --git a/docs/api/frontend-api/imgs/showCollapseFalse.png b/docs/api/frontend-api/imgs/showCollapseFalse.png new file mode 100644 index 000000000..1d43fd695 Binary files /dev/null and b/docs/api/frontend-api/imgs/showCollapseFalse.png differ diff --git a/docs/api/frontend-api/imgs/showCollapseTrue.png b/docs/api/frontend-api/imgs/showCollapseTrue.png new file mode 100644 index 000000000..8b0a2d5f5 Binary files /dev/null and b/docs/api/frontend-api/imgs/showCollapseTrue.png differ diff --git a/docs/api/frontend-api/imgs/showLineFalse.png b/docs/api/frontend-api/imgs/showLineFalse.png new file mode 100644 index 000000000..fc26d090b Binary files /dev/null and b/docs/api/frontend-api/imgs/showLineFalse.png differ diff --git a/docs/api/frontend-api/imgs/showLineTrue.png b/docs/api/frontend-api/imgs/showLineTrue.png new file mode 100644 index 000000000..c321d20b3 Binary files /dev/null and b/docs/api/frontend-api/imgs/showLineTrue.png differ diff --git a/docs/api/frontend-api/registry-api.md b/docs/api/frontend-api/registry-api.md new file mode 100644 index 000000000..fd465c9b5 --- /dev/null +++ b/docs/api/frontend-api/registry-api.md @@ -0,0 +1,332 @@ +# 注册表 API + +注册表 API 是 TinyEngine 的核心功能模块,提供了元应用、元服务的注册、管理、查询和通信能力。本文档详细介绍所有可用的 API 接口。 + +## 核心注册表 API + +### getMergeMeta + +根据 ID 获取合并后的元应用或元服务配置。 + +```javascript +import { getMergeMeta } from '@opentiny/tiny-engine' + +// 获取特定元应用 +const layoutPlugin = getMergeMeta('engine.layout') +const materialsPlugin = getMergeMeta('engine.plugins.materials') +// 获取自己自定义的插件 +const customPlugin = getMergeMeta('engine.customPlugin') +``` + +**参数:** +- `id`: 元应用或元服务的唯一标识符 + +**返回值:** +- 元应用或元服务的完整配置对象,如果不存在则返回 `undefined` + +### getMetaApi + +获取元应用或元服务提供的 API 接口。 + +```javascript +import { getMetaApi } from '@opentiny/tiny-engine' + +// 获取完整 API 对象 +const globalServiceApi = getMetaApi('engine.service.globalService') + +// 获取特定 API 方法 +const getPageList = getMetaApi('engine.service.globalService', 'getPageList') +``` + +**参数:** +- `id`: 元应用或元服务的 ID +- `key` (可选): 特定 API 方法名 + +**返回值:** +- 当不提供 `key` 时,返回完整的 API 对象 +- 当提供 `key` 时,返回对应的 API 方法 +- 如果不存在则返回 `undefined` + +### getOptions + +获取元应用或元服务的配置选项。 + +```javascript +import { getOptions } from '@opentiny/tiny-engine' + +// 获取布局配置选项 +const layoutOptions = getOptions('engine.layout') +``` + +**参数:** +- `id`: 元应用或元服务的 ID + +**返回值:** +- 配置选项对象,如果不存在则返回 `undefined` + +### getMergeMetaByType + +根据类型获取所有对应的元应用或元服务。 + +```javascript +import { getMergeMetaByType } from '@opentiny/tiny-engine' + +// 获取所有插件 +const services = getMergeMetaByType('plugins') +``` + +**参数:** +- `type`: 元应用或元服务的类型 + +**返回值:** +- 匹配类型的元应用或元服务数组 + +### getAllMergeMeta + +获取所有已注册的元应用和元服务。 + +```javascript +import { getAllMergeMeta } from '@opentiny/tiny-engine' + +// 获取所有注册的元应用和元服务 +const allMetas = getAllMergeMeta() +``` + +**返回值:** +- 所有已注册元应用和元服务的数组 + +## 消息通信 API + +### useMessage + +提供事件订阅和发布机制,实现组件间通信。 + +```javascript +import { useMessage } from '@opentiny/tiny-engine' + +export default { + setup() { + const { subscribe, publish, unsubscribe, broadcast } = useMessage() + + // 订阅消息 + const subscription = subscribe({ + topic: 'schemaChange', + subscriber: 'my-plugin', + callback: (data) => { + console.log('Schema changed:', data) + } + }) + + // 发布消息 + const handleSave = () => { + publish({ + topic: 'schemaChange', + data: { operation: 'save', timestamp: Date.now() } + }) + } + + // 广播消息(会存储为最后一条消息) + const handleBroadcast = () => { + broadcast({ + topic: 'globalUpdate', + data: { type: 'system', message: 'System updated' } + }) + } + + // 取消订阅 + onUnmounted(() => { + unsubscribe(subscription) + // 或者 + unsubscribe({ topic: 'schemaChange', subscriber: 'my-plugin' }) + }) + + return { + handleSave, + handleBroadcast + } + } +} +``` + +**API 方法:** + +#### subscribe + +订阅消息。 + +**参数:** +- `options.topic`: 消息主题 +- `options.subscriber` (可选): 订阅者标识 +- `options.callback`: 消息回调函数 + +**返回值:** +- 订阅信息对象 `{ topic, subscriber }` + +#### publish + +发布消息到指定主题的所有订阅者。 + +**参数:** +- `options.topic`: 消息主题 +- `options.data`: 消息数据 + +#### broadcast + +广播消息,与 `publish` 类似,但会存储为最后一条消息,新的订阅者会自动收到。 + +**参数:** +- `options.topic`: 消息主题 +- `options.data`: 消息数据 + +#### unsubscribe + +取消消息订阅。 + +**参数:** +- `options.topic`: 消息主题 +- `options.subscriber` (可选): 订阅者标识,不传递 subscriber时,会取消所有订阅 + +## 服务定义 API + +### defineService + +定义一个新的元服务。 + +```javascript +import { defineService } from '@opentiny/tiny-engine' + +const MyService = defineService({ + id: 'engine.service.myService', + type: 'MetaService', + initialState: { + count: 0, + items: [] + }, + options: { + enableCache: true + }, + init: ({ state, options }) => { + // 服务初始化逻辑 + console.log('Service initialized with options:', options) + }, + apis: { + increment: (amount = 1) => { + const currentState = MyService.apis.getState() + MyService.apis.setState({ count: currentState.count + amount }) + }, + getItems: () => { + return MyService.apis.getState().items + }, + addItem: (item) => { + const currentState = MyService.apis.getState() + MyService.apis.setState({ + items: [...currentState.items, item] + }) + } + } +}) +``` + +**参数:** +- `serviceOptions.id`: 服务唯一标识 +- `serviceOptions.type`: 必须为 'MetaService' +- `serviceOptions.initialState`: 初始状态对象 +- `serviceOptions.options`: 服务配置选项 +- `serviceOptions.init`: 初始化函数 +- `serviceOptions.apis`: API 方法定义(对象或函数) + +**返回值:** +- 服务对象,包含 `id`、`type`、`options` 和 `apis` 属性 + +**内置 API:** +- `getState()`: 获取当前状态(只读) +- `setState(kv)`: 更新状态 +- `setOptions(kv)`: 更新配置选项 + +## 配置器 API + +### addConfigurator + +添加属性配置器组件。 + +```javascript +import { addConfigurator } from '@opentiny/tiny-engine' +import InputConfigurator from './InputConfigurator.vue' +import SelectConfigurator from './SelectConfigurator.vue' + +addConfigurator([ + { + name: 'InputConfigurator', + component: InputConfigurator + }, + { + name: 'SelectConfigurator', + component: SelectConfigurator + } +]) +``` + +**参数:** +- `components`: 配置器组件数组,每个元素包含 `name` 和 `component` + +### getConfigurator + +获取指定名称的配置器组件。 + +```javascript +import { getConfigurator } from '@opentiny/tiny-engine' + +const InputConfigurator = getConfigurator('InputConfigurator') +``` + +**参数:** +- `name`: 配置器名称 + +**返回值:** +- 配置器组件,如果不存在则返回 `undefined` + +## 高级功能 API + +### initHotfixRegistry + +获取并初始化热修复注册表。 + +```javascript +import { initHotfixRegistry } from '@opentiny/tiny-engine-meta-register' + +// 从远程 URL 加载热修复注册表 +await initHotfixRegistry({ + url: 'https://example.com/hotfix-registry.js' +}) +``` + +**参数:** +- `options.url`: 热修复注册表文件的 URL +- `options.request` (可选): 自定义请求函数 + +## 常量定义 + +### META_SERVICE + +定义了所有内置元服务的 ID 常量。 + +```javascript +import { META_SERVICE } from '@opentiny/tiny-engine' + +console.log(META_SERVICE.GlobalService) // 'engine.service.globalService' +console.log(META_SERVICE.Layout) // 'engine.service.layout' +// ... 更多服务常量 +``` + +### META_APP + +定义了所有内置元应用的 ID 常量。 + +```javascript +import { META_APP } from '@opentiny/tiny-engine' + +console.log(META_APP.Layout) // 'engine.layout' +console.log(META_APP.Materials) // 'engine.plugins.materials' +console.log(META_APP.Canvas) // 'engine.canvas' +// ... 更多应用常量 +``` diff --git a/docs/catalog.json b/docs/catalog.json index a2dd86f60..db74af241 100644 --- a/docs/catalog.json +++ b/docs/catalog.json @@ -81,6 +81,15 @@ { "title": "前后端启动联调(Java服务端)", "name": "debugging-of-java-backend.md" } ] }, + + { + "title": "更新日志", + "name": "changelog", + "articles": [ + { "title": "更新日志", "name": "changelog.md" }, + { "title": "v2.6升级指南", "name": "v2.6-upgrade-guide.md" } + ] + }, { "title": "解决方案", "name": "solutions", @@ -102,6 +111,8 @@ "articles": [ { "title": "新架构介绍", "name": "new-architecture.md" }, { "title": "注册表", "name": "registry.md" }, + { "title": "注册表(新版)", "name": "new-registry.md" }, + { "title": "注册表高级特性", "name": "new-registry-advanced.md" }, { "title": "元服务和元应用", "name": "meta-services-and-meta-apps.md" } ] }, diff --git a/docs/changelog/changelog.md b/docs/changelog/changelog.md new file mode 100644 index 000000000..b11faeeb4 --- /dev/null +++ b/docs/changelog/changelog.md @@ -0,0 +1,35 @@ +# 更新日志 + +## v2.6 + +### 升级指南 + +[v2.6 升级指南](./v2.6-upgrade-guide.md) + +### What's Change + +#### 🎉 Exciting New Features + +- **新的注册表声明方式**:采用基于唯一 ID 的扁平结构,更加灵活和精确 +- **布局配置优化**:支持 `layoutConfig` 和 `relativeLayoutConfig` 两种布局配置方式 +- **注册表热修复功能**:通过覆盖官方插件的特定函数或模板,实现紧急 bug 修复 +- **默认注册表内置**:内置全量默认注册表,无需重复声明未修改的插件 + +#### 🐛 Bug Fixes + +- 修复了多个与注册表相关的问题 +- 优化了插件的加载和初始化流程 + +#### 📚 Documentation + +- 新增 [v2.6 升级指南](./v2.6-upgrade-guide.md) +- 新增 [新注册表](../extension-capabilities-overview/new-registry.md) 文档 +- 新增 [注册表高级配置](../extension-capabilities-overview/new-registry-advanced.md) 文档 + +#### ⚙️ Other changes + +- 废弃插件 `align` 配置,改用布局配置来定位插件 +- 废弃插件 `type: setting` 配置,统一使用 `type: plugins` +- 优化注册表合并机制,提高性能 + +## 完整的更新日志,请前往 [GitHub Release](https://github.com/opentiny/tiny-engine/releases) 查看。 diff --git a/docs/changelog/v2.6-upgrade-guide.md b/docs/changelog/v2.6-upgrade-guide.md new file mode 100644 index 000000000..c373eb9bf --- /dev/null +++ b/docs/changelog/v2.6-upgrade-guide.md @@ -0,0 +1,413 @@ +# v2.6 升级指南 + +> 本文档为 v2.6 版本的升级指南,主要介绍了 v2.6 版本的主要新特性、升级步骤和注意事项。 +> +> 升级前请先阅读 [v2.6 更新日志](./changelog.md),了解 v2.6 版本的变更内容。 + +## 主要变更内容 + +### 1. 注册表声明方式变更 + +v2.6 版本对注册表的声明方式进行了重大调整,采用了基于唯一 ID 的新注册方式,使配置更加灵活和精确。 + +#### 旧版注册表方式 + +```javascript +// 旧版注册表配置示例 +const register = { + root: { + id: 'engine.root', + metas: [GenerateCodeService, GlobalService] + }, + config: engineConfig, + layout: { + ...Layout + options: {...} + }, + themes: [ + { + id: 'engine.theme.light' + }, + { + id: 'engine.theme.dark' + } + ], + toolbars: [Media, Save], + plugins: [Materials, Tree], + settings: [Props, Styles], + Canvas: Canvas +} +``` + +#### 新版注册表方式 + +```javascript +import { META_APP } from '@opentiny/tiny-engine' +// 新版注册表配置示例 +const register = { + 'engine.root': { + id: 'engine.root', + metas: [GenerateCodeService, GlobalService] + }, + 'engine.config': engineConfig, + // 覆盖官方的配置 + [META_APP.Layout]: { + options: {...} + }, + // 配置 false 隐藏工具栏清空按钮,并且在构建的时候,会将工具栏插件的相关代码做 tree-shaking + [META_APP.Clean]: false, + // 替换整个页面JS插件,手动配置 tree-shaking 为 true,会将原来的页面JS插件的代码做 tree-shaking + /* #__TINY_ENGINE_TREE_SHAKING__: true */ + [META_APP.Script]: scriptPlugin, + // 新增的插件,需要使用与官方插件不相同的唯一 id + 'engine.plugins.customPlugin': { + ...customPlugin, + id: 'engine.plugins.customPlugin' + } +} +``` + +#### 升级步骤 + +1. 将注册表配置从旧的分类式结构(`toolbars`, `plugins`, `settings`等)调整为基于唯一 ID 的扁平结构 +2. 对于已存在的官方组件,使用其唯一 ID 作为对象的键 +3. 移除不需要的插件时,将其值设置为 `false` +4. 添加新插件时,确保使用与官方插件不同的唯一 ID + +> **重要提示⚠️**:v2.6 开始,如果对原插件没有改动(配置、替换、删除),则不需要在注册表中进行声明,因为官方内置了全量的注册表。 + +详细内容请参考 [新注册表](../extension-capabilities-overview/new-registry.md)。 + +## API 变更 + +### @opentiny/tiny-engine-meta-register 包 API 变化 + +v2.6 版本对 `@opentiny/tiny-engine-meta-register` 包的 API 进行了重大重构,以下是主要变化: + +#### 移除的 API + +##### 1. `getMergeRegistry` 函数 + +**变更说明**:`getMergeRegistry` 函数已被完全移除,不再提供此 API。 + +**旧版用法**: +```javascript +import { getMergeRegistry } from '@opentiny/tiny-engine' + +// 根据类型和 ID 获取合并后的注册表项 +const plugin = getMergeRegistry('plugins', 'engine.plugins.materials') +const allPlugins = getMergeRegistry('plugins') +``` + +**新版替代方案**: +```javascript +import { getMergeMeta, getMergeMetaByType, getAllMergeMeta } from '@opentiny/tiny-engine' + +// 根据 ID 获取特定的注册表项 +const plugin = getMergeMeta('engine.plugins.materials') + +// 根据类型获取所有注册表项 +const allPlugins = getMergeMetaByType('plugins') + +// 获取所有注册表项 +const allMetas = getAllMergeMeta() +``` + +##### 2. `getLayoutComponent` 函数 + +**变更说明**:`getLayoutComponent` 函数已被移除,不再单独提供布局组件获取功能。 + +**旧版用法**: +```javascript +import { getLayoutComponent } from '@opentiny/tiny-engine-meta-register' + +const layoutComponent = getLayoutComponent({ id: 'engine.layout.header' }) +``` + +**新版替代方案**: +布局组件现在通过新的注册表机制进行管理,应使用 `getMergeMeta` 获取: + +```javascript +import { getMergeMeta } from '@opentiny/tiny-engine' + +const layoutMeta = getMergeMeta('engine.layout') +``` + +#### 新增的 API + +##### 1. `getMergeMetaByType` 函数 + +**功能说明**:根据类型获取所有匹配的注册表项。 + +```javascript +import { getMergeMetaByType } from '@opentiny/tiny-engine' + +// 获取所有插件类型的注册表项 +const plugins = getMergeMetaByType('plugins') +``` + +##### 2. `getAllMergeMeta` 函数 + +**功能说明**:获取所有的注册表项。 + +```javascript +import { getAllMergeMeta } from '@opentiny/tiny-engine' + +// 获取完整的注册表 +const allMetas = getAllMergeMeta() +``` + +##### 3. `initHotfixRegistry` 函数 + +**功能说明**:初始化热修复注册表,支持远程动态加载注册表配置。 + +```javascript +import { initHotfixRegistry } from '@opentiny/tiny-engine-meta-register' + +// 从远程 URL 加载热修复注册表 +await initHotfixRegistry({ + url: 'https://example.com/hotfix-registry.js' +}) +``` + +#### 升级指导 + +1. **替换 `getMergeRegistry` 调用**: + - 将 `getMergeRegistry(type, id)` 替换为 `getMergeMeta(id)` + - 将 `getMergeRegistry(type)` 替换为 `getMergeMetaByType(type)` + +2. **移除 `getLayoutComponent` 调用**: + - 使用 `getMergeMeta()` 获取布局相关的注册表项 + - 通过新的布局配置机制管理布局组件 + +3. **利用新增 API**: + - 使用 `getAllMergeMeta()` 获取完整注册表信息,便于调试和开发 + - 使用 `initHotfixRegistry()` 实现热修复功能 + +详细API请参考 [注册表 API](../api/frontend-api/registry-api.md)。 + +## Vite 配置要求 + +### registryPath 配置 + +**重要说明⚠️**:v2.6 版本开始,为了使注册表的 tree-shaking 功能正常工作,您需要在 `vite.config.js` 中配置 `registryPath` 参数。 + +```javascript +// vite.config.js +import { defineConfig, mergeConfig } from 'vite' +import { useTinyEngineBaseConfig } from '@opentiny/tiny-engine-vite-config' + +export default defineConfig((configEnv) => { + const baseConfig = useTinyEngineBaseConfig({ + viteConfigEnv: configEnv, + root: __dirname, + // 其他配置... + registryPath: './registry.js' // 必须配置,指向注册表文件路径 + }) + + // 其他配置... + return mergeConfig(baseConfig, customConfig) +}) +``` + +这个配置主要用于: +- 支持插件的 tree-shaking 优化 +- 识别被设置为 `false` 的插件并在构建时移除相关代码 +- 解析注册表中的特殊注释指令 + +### 2. 布局(layout)变更 + +v2.6 版本对布局配置进行了优化,使布局更加灵活可配置。 + +#### 布局配置变更 + +新版布局配置提供了两种配置方式: +- `layoutConfig`:完整布局配置,自定义整个布局结构 +- `relativeLayoutConfig`:局部调整插件顺序,适用于仅调整部分插件位置 + +```javascript +// 布局配置示例 +export default { + 'engine.layout': { + options: { + // 完整布局配置(二选一) + layoutConfig: { + plugins: { + left: { + top: [/* 左侧顶部插件列表 */], + bottom: [/* 左侧底部插件列表 */] + }, + right: { + top: [/* 右侧顶部插件列表 */] + } + }, + toolbars: { + left: [/* 左侧工具栏 */], + center: [/* 中间工具栏 */], + right: [/* 右侧工具栏(二维数组支持分组) */], + collapse: [/* 折叠菜单(二维数组支持分组) */] + } + }, + + // 局部调整插件顺序(二选一) + relativeLayoutConfig: { + // 插件相对位置配置 + 'engine.plugins.customPlugin': { + insertBefore: 'engine.plugins.materials' // 显示在物料面板前面 + }, + 'engine.plugins.anotherPlugin': { + insertAfter: 'engine.plugins.i18n' // 显示在国际化插件后面 + } + } + } + } +} +``` + +> **注意⚠️**:当同时配置 `layoutConfig` 和 `relativeLayoutConfig` 时,`layoutConfig` 优先级更高,`relativeLayoutConfig` 将被忽略。 + +详细内容请参考 [布局配置](../api/frontend-api/global-layout-api.md)。 + +### 3. 插件配置变更 + +#### 3.1 插件 `align` 配置的废弃 + +在 v2.6 版本中,插件的 `align` 配置属性已被废弃,不再作为定位插件位置的方式。新版本中应使用 `layoutConfig` 或 `relativeLayoutConfig` 来定位插件。 + +旧版写法: +```javascript +// 不再支持的写法 +const plugin = { + id: 'engine.plugins.customPlugin', + align: 'leftTop' // 已废弃 + // 其他配置... +} +``` + +新版写法: +```javascript +// 新写法 - 使用 layoutConfig 定位插件 +const register = { + 'engine.layout': { + options: { + layoutConfig: { + plugins: { + left: { + top: ['engine.plugins.customPlugin', /* 其他插件... */] + } + } + } + } + } +} + +// 或使用 relativeLayoutConfig +const register = { + 'engine.layout': { + options: { + relativeLayoutConfig: { + 'engine.plugins.customPlugin': { + insertBefore: 'engine.plugins.materials' + } + } + } + } +} +``` + +#### 3.2 插件 `type: setting` 配置的废弃 + +在 v2.6 版本中,右侧设置面板插件不再使用 `type: 'setting'` 来标识,而是统一使用插件 ID 来区分。 + +旧版写法: +```javascript +// 不再支持的写法 +export default { + id: 'engine.setting.props', + title: '属性', + type: 'settings', // 已废弃 + name: 'props', + icon: 'form' +} +``` + +新版写法: +```javascript +// 新写法 +export default { + id: 'engine.setting.props', + title: '属性', + type: 'plugins', // 统一使用 plugins 类型 + name: 'props', + icon: 'form' +} +``` + +右侧设置面板的插件现在也通过布局配置的 `layoutConfig` 或 `relativeLayoutConfig` 进行定位: + +```javascript +const register = { + 'engine.layout': { + options: { + layoutConfig: { + plugins: { + right: { + top: ['engine.setting.props', 'engine.setting.styles', 'engine.setting.event'] + } + } + } + } + } +} +``` + +### 4. 注册表热修复功能 + +v2.6 版本新增了注册表热修复(hotfix)功能,可以通过覆盖官方插件的特定函数或模板,实现紧急 bug 修复,而不需要等待官方版本发布。 + +```javascript +// hotfix 注册表示例 +export default { + 'engine.plugins.i18n': { + overwrite: { + lifeCycles: { + 'Main': { + onMounted: [ + (ctx) => () => { + // 覆盖 i18n 插件的 onMounted 生命周期方法 + const { i18nSearchTypes, currentSearchType } = ctx() + currentSearchType.value = i18nSearchTypes[0].value + } + ] + } + } + } + } +} +``` + +详细内容请参考 [注册表高级配置](../extension-capabilities-overview/new-registry-advanced.md)。 + +## 其他改进 + +- **默认注册表内置**:v2.6 版本内置了全量的默认注册表,如果对原插件没有改动(配置、替换、删除),则不需要在注册表中进行声明。 + +## 升级步骤建议 + +1. 检查当前项目中的注册表配置,将其调整为新的基于唯一 ID 的扁平结构 +2. 更新布局配置,使用 `layoutConfig` 或 `relativeLayoutConfig` 来定位插件 +3. 移除插件中的 `align` 配置和 `type: 'setting'` 配置 +4. 如有必要,使用注册表热修复功能解决紧急问题 + +## 常见问题 + +### 如何查找插件的唯一 ID? + +可以参考官方默认的全局注册表:[默认注册表](https://github.com/opentiny/tiny-engine/blob/develop/packages/design-core/registry.js) + +### 我的插件在升级后无法正常显示 + +请检查以下几点: +1. 插件的唯一 ID 是否正确 +2. 布局配置中是否包含了该插件 +3. 插件是否被设置为 `false` diff --git a/docs/extension-capabilities-overview/new-registry-advanced.md b/docs/extension-capabilities-overview/new-registry-advanced.md new file mode 100644 index 000000000..80ca0d3e3 --- /dev/null +++ b/docs/extension-capabilities-overview/new-registry-advanced.md @@ -0,0 +1,255 @@ +# 注册表高级配置 + + +## 注册表 hotfix 功能,实现紧急 bug 修复功能 + +> 注:该功能可用版本:2.6.0+ +> +> ⚠️ 该功能应该仅作为紧急 bug 修复使用,不应该滥用,一旦官方已经修复 bug,请及时移除 hotfix 注册表。 + +背景:开源的开发过程中,难免会遇到一些紧急的 bug 需要修复,如果等待开源版本的下个版本发布,可能需要经过这样一个流程: + +1. 用户向TinyEngine团队反馈 bug。(30min - 1h) +2. TinyEngine团队分析 bug 原因,并给出修复方案。(1h - 2h) +3. 验证修复方案,发布新版本。(1h) +4. 用户同步新版本,验证新版本。(1h-2h) +5. 用户确认无误,提交审批流程给领导,发布新版本。(1h-2h) +6. 新版本上线,用户可以正常使用。(1h-2h) + +经过上述的一个流程可以看到,整个标准的修复流程相对比较长,如果是一些对用户影响比较大的问题,在商业上可能无法满足要求。 + +因此,我们推出了注册表的 hotfix 功能,可以通过传入 hotfix 的注册表,对某些插件实现函数级别的覆盖能力,从而实现快速修复紧急 bug。 + +### hotfix 注册表功能使用示例: + +1. 在后端增加一个接口,返回临时的 hotfix 注册表。比如 `/hotfix-registry.js`。没有紧急 bug 的时候,返回空对象。 + +2. 在 TinyEngine 初始化的时候,调用这个接口,获取临时的 hotfix 注册表。 + +```javascript +// 这里获取线上的注册表 +const fetchHotfixRegistry = async (url) => { + const response = await import(/* @vite-ignore */ url) + return response.default +} + +async function startApp() { + // 调用 initHotfixRegistry 方法,传入接口地址以及请求方法,获取临时的 hotfix 注册表并提前注册。 + const hotfixRegistry = + (await initHotfixRegistry({ + url: 'http://localhost:8090/hotfixRegistry.js', + request: fetchHotfixRegistry + })) || {} + + const registry = await import('../registry') + const { init } = await import('@opentiny/tiny-engine') + + init({ + // 合并多个注册表 + registry: [registry.default, hotfixRegistry], + configurators, + createAppSignal: ['global_service_init_finish'] + }) +} + +startApp() +``` + +示例 hotfix 注册表: + +```javascript +// hotfixRegistry.js +export default { + 'engine.plugins.i18n': { + overwrite: { + methods: { + 'Main': { + // 覆盖 i18n 插件的 openEditor 方法 + openEditor: (ctx) => (_event, row) => { + const { isEditMode, editingRow, i18nTable, langList, getActiveRow, utils } = ctx() + isEditMode.value = Boolean(row.key) + editingRow.value = row + if (!isEditMode.value) { + row.key = `custom.${utils.guid()}` + langList.value.unshift(row) + } + i18nTable.value.setActiveRow(row).then(() => { + getActiveRow() + }) + } + } + }, + lifeCycles: { + 'Main': { + onMounted: [ + // i18n 插件 Main.vue 文件的第一个 onMounted 方法,不覆盖 + '', + // 覆盖 i18n 插件 Main.vue 文件的第二个 onMounted 方法 + (ctx) => () => { + const { i18nSearchTypes, currentSearchType } = ctx() + console.log('overWrite i18n onMounted', i18nSearchTypes, currentSearchType.value) + currentSearchType.value = i18nSearchTypes[0].value + } + ] + } + } + } + } +} +``` + +### 注册表 hotfix 功能说明 + +#### 注册表的 hotfix 功能,需要提前注册,因为 overWrite 的逻辑需要提前读取。 + +即 initHotfixRegistry 方法的调用,必须在 registry 以及 init 方法之前。(所以 注册表 以及 init 方法都需要改成异步的 import) + +```javascript +async function startApp() { + + const hotfixRegistry = + (await initHotfixRegistry({ + url: 'http://localhost:8090/hotfixRegistry.js', + request: fetchHotfixRegistry + })) || {} + + const registry = await import('../registry') + const { init } = await import('@opentiny/tiny-engine') + + init({ + // 合并多个注册表 + registry: [registry.default, hotfixRegistry], + configurators, + createAppSignal: ['global_service_init_finish'] + }) +} +``` + +#### hotfix 注册表的覆盖能力 + +1. 覆盖插件的 methods 方法(自定义方法) +2. 覆盖插件的 lifeCycles 方法(vue 生命周期) + +### hotfix 注册表覆盖示例 + +#### 覆盖插件的 methods 方法 + +假如我们希望覆盖 i18n 插件中 Main.vue 文件的 openEditor 方法: + +1. 查看 i18n 插件的 src/Main.vue 文件,发现有 metaService 的注释,确认可以对该文件进行覆盖。(没有 metaService 或者是 metaComponent 注释的文件,无法进行覆盖) + +metaService 或者是 metaComponent 注释的格式如下: + +```javascript +/* metaService: engine.plugins.i18n.Main */ + +/* metaComponent: engine.plugins.i18n.Main */ +``` + +2. 查看 Main.vue 文件的 metaService 注释,确认 id 为 engine.plugins.i18n.Main。我们将这个 id 拆分成两个部分: + +a. 插件 id:engine.plugins.i18n +b. 文件 id:Main + +3. 根据插件 id 和文件id,我们就可以确定配置的相关 key。于是:我们就可以得到如下代码: + +```javascript +export default { + 'engine.plugins.i18n': { + overwrite: { + methods: { + 'Main': { + openEditor: (ctx) => (_event, row) => { + const { isEditMode, editingRow, i18nTable, langList, getActiveRow, utils } = ctx() + isEditMode.value = Boolean(row.key) + editingRow.value = row + if (!isEditMode.value) { + row.key = `custom.${utils.guid()}` + langList.value.unshift(row) + } + i18nTable.value.setActiveRow(row).then(() => { + getActiveRow() + }) + } + } + } + } + } +} +``` + +代码解析: +- 'engine.plugins.i18n',指定我们要配置 i18n 插件。 +- overwrite 指定我们要使用覆盖功能。 +- methods 指定我们要覆盖 i18n 插件的 methods 方法。 +- 'Main',指定我们要覆盖 i18n 插件的 Main.vue 文件。 +- openEditor 指定我们要覆盖 i18n 插件的 openEditor 方法。 + +方法覆盖说明: +- 方法覆盖的格式为:`方法名: (ctx) => (_event, row) => { ... }`。 +- ctx 为上下文对象方法,通过 ctx() 获取,可以得到原来的上下文对象。 +- _event, row 为原来方法形参(入参),不可以覆盖。 +- `{...}` 为新方法的实现,在这里实现函数覆盖的逻辑。 + +##### 覆盖插件的 lifeCycles 方法 + +假如我们希望覆盖 i18n 插件的 onMounted 方法: + +1. 查看 i18n 插件的 src/Main.vue 文件,发现有 metaService 的注释,确认可以对该文件进行覆盖。(没有 metaService 或者是 metaComponent 注释的文件,无法进行覆盖) + +2. 查看 Main.vue 文件的 metaService 注释,确认 id 为 engine.plugins.i18n.Main。我们将这个 id 拆分成两个部分: + +a. 插件 id:engine.plugins.i18n +b. 文件 id:Main + +3. 根据插件 id 和文件id,我们就可以确定配置的相关 key。于是:我们就可以得到如下代码: + +```javascript +export default { + 'engine.plugins.i18n': { + overwrite: { + lifeCycles: { + 'Main': { + onMounted: [ + (ctx) => () => { + const { i18nSearchTypes, currentSearchType } = ctx() + console.log('overWrite i18n onMounted', i18nSearchTypes, currentSearchType.value) + currentSearchType.value = i18nSearchTypes[0].value + } + ] + } + } + } + } +} +``` + +代码解析: + +- 'engine.plugins.i18n',指定我们要配置 i18n 插件。 +- overwrite 指定我们要使用覆盖功能。 +- lifeCycles 指定我们要覆盖 i18n 插件的 lifeCycles 方法 (vue 生命周期)。 +- 'Main',指定我们要覆盖 i18n 插件的 Main.vue 文件。 +- onMounted 指定我们要覆盖 i18n 插件的 onMounted 方法,由于 onMounted 方法可能会声明多次,所以我们这里需要使用数组,需要覆盖第几次 onMounted 方法,就在数组对应的排列顺序上写覆盖方法,如果前面的不需要覆盖,则写空字符串。 + +比如: + +```javascript +onMounted: [ + '', + '', + '', + // 这里覆盖第4次 onMounted 方法 + 'onMounted: (ctx) => () => { ... }' +] +``` + +现在,让我们再来看看使用了 hotfix 注册表之后的修复流程: + +1. 二开用户向TinyEngine团队反馈 bug。(30min - 1h) +2. TinyEngine 分析 bug 原因,并给出修复方案。(1h - 2h) +3. 二开用户使用 hotfix 注册表功能,覆盖官方的某个函数或者是模板。(10min) +4. 用户验证修复方案,推送到生产环境注册表。(1h) +5. 生产环境生效,用户正常使用。 + +可以看到,使用 hotfix 注册表之后,修复流程大大缩短,大大提高了修复效率。 diff --git a/docs/extension-capabilities-overview/new-registry.md b/docs/extension-capabilities-overview/new-registry.md new file mode 100644 index 000000000..ae13b3b80 --- /dev/null +++ b/docs/extension-capabilities-overview/new-registry.md @@ -0,0 +1,310 @@ +# 注册表(新) + +⚠️注意:该文档仅适用于 TinyEngine v2.6+ 版本,如果需要了解旧的注册表配置方式,请参考 [旧注册表](./registry.md)。 + +## 什么是注册表 + +在 新架构介绍中,我们引入了注册表的概念,二次低代码平台开发用户通过注册表配置元服务元应用,TinyEngine底层引擎读取注册表的配置,完成元应用元服务的定制,然后加载对应的元应用元服务,完成低代码平台的启动。 + +所以注册表就是完成元应用元服务注册、配置、覆盖的TinyEngine提供的底层核心功能。 + +注册表的作用: + +- 接收元应用元服务的配置,传递到低代码底层引擎,完成低代码平台的定制化。 +- 合并默认的元应用元服务的配置项以及用户的自定义配置项。 +- 提供查询能力,使得元服务与元服务之间能够相互通信,或者相关状态变量。 + +## 注册表配置结构 + +传入到TinyEngine底层引擎的示例: + +```javascript +import { META_APP } from '@opentiny/tiny-engine' +// 注册表配置示例 +const register = { + 'engine.root': { + id: 'engine.root', + metas: [GenerateCodeService, GlobalService] + }, + 'engine.config': engineConfig, + // 覆盖官方的配置 + [META_APP.Layout]: { + options: {...} + }, + // 配置 false 隐藏工具栏清空按钮,并且在构建的时候,会将工具栏插件的相关代码做 tree-shaking + [META_APP.Clean]: false, + // 替换整个页面JS插件,手动配置 tree-shaking 为 true,会将原来的页面JS插件的代码做 tree-shaking + /* #__TINY_ENGINE_TREE_SHAKING__: true */ + [META_APP.Script]: scriptPlugin, + // 新增的插件,需要使用与官方插件不相同的唯一 id + 'engine.plugins.customPlugin': { + ...customPlugin, + id: 'engine.plugins.customPlugin' + } +} +``` + +示例解读: +1. 最外层为一个对象结构,每个键都是一个唯一的注册表ID。 +2. `engine.root`:配置核心的元服务,许多的插件依赖这些核心的元服务。 +3. `engine.config`:低代码引擎的配置,主要配置物料、主题等等。 +4. `[META_APP.Layout]`:配置低代码引擎的布局,可以通过扩展官方布局来自定义。 +5. `[META_APP.Clean]: false`:通过设置为 `false` 来隐藏特定工具栏按钮,同时在构建时会进行 tree-shaking 优化。 +6. `[META_APP.Script]`:替换整个页面JS插件,并通过注释 `#__TINY_ENGINE_TREE_SHAKING__: true` 指示构建工具对原插件代码进行 tree-shaking。 +7. `engine.plugins.customPlugin`:添加新的自定义插件,需要使用与官方插件不同的唯一ID。 + +通过这种基于ID的注册方式,可以更精细地控制平台的各个部分,实现添加、替换或移除特定功能,而不需要重新配置整个注册表结构。 + + +注意⚠️:v2.6 开始,如果对原插件没有改动(配置、替换、删除),则不需要在注册表中进行声明,因为官方内置了全量的注册表: + +官方默认的全局注册表,请参考 [默认注册表](https://github.com/opentiny/tiny-engine/blob/develop/packages/design-core/registry.js)。 + +## 注册表使用 + +### 初始化时传入注册表 + +我们可以在初始化的时候传入注册表,初始化的时候会合并默认的注册表和传入的注册表,然后生成新的注册表。并根据注册表的配置,完成低代码平台的定制化。 + +1. 在 registry.js 中声明注册表 + +```javascript +// registry.js +import scriptPlugin from './src/plugins/script' + +export default { + [META_SERVICE.Http]: HttpService, + 'engine.config': { + ...engineConfig + }, + // 删除工具栏清空按钮 + [META_APP.Clean]: false, + // 替换整个页面JS插件 + [META_APP.Script]: scriptPlugin, + // 传入插件配置 + [META_APP.Layout]: { + options: { + relativeLayoutConfig: { + // ... + } + } + } +} +``` + +2. 调用 init 方法传入注册表,初始化TinyEngine + +```javascript +async function startApp() { + const registry = await import('../registry') + const { init } = await import('@opentiny/tiny-engine') + + init({ + // 传入注册表 + registry: [registry.default], + // 配置器 + configurators, + // 其他配置项 + createAppSignal: ['global_service_init_finish'] + }) +} + +// 初始化 TinyEngine +startApp() +``` + + +### 运行时使用注册表 + +在 TinyEngine 启动起来之后,我们可以通过注册表提供的能力,获取到元服务、元应用、配置,并进行事件订阅、插件之间的通信等等。 + +#### 获取元服务或元应用 + +```javascript +import { getMergeMeta, getMetaApi } from '@opentiny/tiny-engine' + +export default { + setup() { + // 获取物料面板插件 + const materialsPlugin = getMergeMeta('engine.plugins.materials') + + // 获取物料面板插件的入口组件 + const materialsEntry = materialsPlugin?.entry + + // 获取全局服务的 API + const globalServiceApi = getMetaApi('engine.service.globalService') + + // 获取生成代码服务的 API + const generateCodeApi = getMetaApi('engine.service.generateCode') + + // 获取页面JS插件 + const pageControllerPlugin = getMergeMeta('engine.plugins.pagecontroller') + } +} +``` + +#### 获取配置 + +```javascript +import { getMergeMeta, getOptions } from '@opentiny/tiny-engine' + +export default { + setup() { + // 获取引擎配置 + const engineConfig = getMergeMeta('engine.config') + + // 获取特定配置项 + const platformId = engineConfig?.platformId + const materials = engineConfig?.materials + const editMode = engineConfig?.editMode + + // 获取布局配置选项 + const layoutOptions = getOptions('engine.layout') + + // 或者通过 getMergeMeta 获取布局配置 + const layoutConfig = getMergeMeta('engine.layout')?.options + + return { + platformId, + materials, + editMode, + layoutOptions, + layoutConfig + } + } +} +``` + +#### 事件订阅与发布 + +```javascript +import { useMessage, getMetaApi } from '@opentiny/tiny-engine' + +export default { + setup() { + // 获取消息订阅发布系统 + const { subscribe, publish, unsubscribe } = useMessage() + + // 订阅事件 + subscribe({ + topic: 'schemaChange', + subscriber: 'custom-plugin', + callback: (data) => { + console.log('schema 发生了变化', data) + } + }) + + // 发布事件 + const notifyPageSaved = () => { + publish({ + topic: 'schemaChange', + data: { + operation: {...} + } + }) + } + + // 组件销毁时取消订阅 + onUnmounted(() => { + unsubscribe({ + topic: 'schemaChange', + subscriber: 'custom-plugin' + }) + }) + + // 获取全局服务也可以使用事件机制 + const globalService = getMetaApi('engine.service.globalService') + if (globalService?.getBaseInfo) { + globalService.getBaseInfo().then((data) => { + console.log('全局应用信息', data) + }) + } + + return { + notifyPageSaved + } + } +} +``` + +#### 使用内置Hook API + +TinyEngine提供了许多内置的Hook API,可以更便捷地访问各种官方的元应用元服务: + +```javascript +import { + useCanvas, + usePage, + useLayout, + useProperties, + useMaterial +} from '@opentiny/tiny-engine' + +export default { + setup() { + // 使用schema服务相关 API + const canvas = useCanvas() + + // 使用页面相关API + const page = usePage() + + // 使用布局相关API + const layout = useLayout() + + // 使用属性面板相关API + const properties = useProperties() + + // 使用物料相关API + const material = useMaterial() + + const toggleSidePanel = (panelName) => { + layout.activePlugin(panelName) + } + + // 详细的插件 API,请参考各个元服务的 API 文档 + } +} +``` + +通过以上示例,可以看到注册表提供了一种统一的方式来获取和操作低代码平台中的各种服务和插件,实现了解耦和灵活的通信机制。这使得开发者可以更容易地扩展和定制 TinyEngine 平台,而不需要深入了解底层实现细节。 + +更多高级特性,请参考 [注册表高级配置](./new-registry-advanced.md)。 + + +## Vite 配置要求 + +**重要说明⚠️**:为了使注册表的 tree-shaking 功能正常工作,您需要在 `vite.config.js` 中配置 `registryPath` 参数,指向您的注册表文件路径。 + +```javascript +// vite.config.js +import { defineConfig, mergeConfig } from 'vite' +import { useTinyEngineBaseConfig } from '@opentiny/tiny-engine-vite-config' + +export default defineConfig((configEnv) => { + const baseConfig = useTinyEngineBaseConfig({ + viteConfigEnv: configEnv, + root: __dirname, + // 其他配置... + registryPath: './registry.js' // 必须配置,指向注册表文件路径 + }) + + const customConfig = { + // 您的自定义配置... + } + + return mergeConfig(baseConfig, customConfig) +}) +``` + +### 为什么需要配置 registryPath? + +1. **Tree-shaking 优化**:TinyEngine 需要在构建时解析注册表文件,识别哪些插件被设置为 `false`,从而在构建时移除相关代码,减小最终打包体积。 + +2. **注释指令解析**:支持解析注册表中的特殊注释(如 `#__TINY_ENGINE_TREE_SHAKING__: true`),实现更精细的代码优化。 + +3. **构建优化**:通过静态分析注册表配置,在构建时就确定最终需要包含的功能模块,提高运行时性能。 + +如果没有配置 `registryPath`,以下功能可能无法正常工作: +- 插件的 tree-shaking 优化 +- 通过设置 `false` 来移除插件的功能 +- 构建时的代码体积优化 diff --git a/docs/extension-capabilities-overview/registry.md b/docs/extension-capabilities-overview/registry.md index f8af68b73..1d5165a70 100644 --- a/docs/extension-capabilities-overview/registry.md +++ b/docs/extension-capabilities-overview/registry.md @@ -1,5 +1,7 @@ # 注册表 +⚠️注意:TinyEngine v2.6 版本之后,注册表的方式有所变化,请参考 [新注册表](./new-registry.md) 了解新的注册表配置方式。 + ## 什么是注册表 在 新架构介绍中,我们引入了注册表的概念,二次低代码平台开发用户通过注册表配置元服务元应用,TinyEngine底层引擎读取注册表的配置,完成元应用元服务的定制,然后加载对应的元应用元服务,完成低代码平台的启动。 diff --git a/packages/build/vite-config/package.json b/packages/build/vite-config/package.json index da9702b61..49945803b 100644 --- a/packages/build/vite-config/package.json +++ b/packages/build/vite-config/package.json @@ -16,10 +16,12 @@ "dependencies": { "@babel/core": "~7.23.2", "@babel/generator": "~7.23.2", + "@babel/parser": "^7.27.2", "@babel/traverse": "~7.23.2", "@esbuild-plugins/node-globals-polyfill": "^0.2.3", "@esbuild-plugins/node-modules-polyfill": "^0.2.2", "@opentiny/tiny-engine-vite-plugin-meta-comments": "workspace:^", + "@rollup/plugin-replace": "^6.0.2", "@types/node": "^18.0.0", "@vitejs/plugin-vue": "^5.1.2", "@vitejs/plugin-vue-jsx": "^4.0.1", diff --git a/packages/build/vite-config/src/default-config.js b/packages/build/vite-config/src/default-config.js index 67a9d3c9c..0c53dcedf 100644 --- a/packages/build/vite-config/src/default-config.js +++ b/packages/build/vite-config/src/default-config.js @@ -14,6 +14,7 @@ import { getBaseUrlFromCli, copyBundleDeps, importMapLocalPlugin } from './local import { devAliasPlugin } from './vite-plugins/devAliasPlugin.js' import { htmlUpgradeHttpsPlugin } from './vite-plugins/upgradeHttpsPlugin.js' import { canvasDevExternal } from './canvas-dev-external.js' +import { treeShakingPlugin } from './vite-plugins/treeShakingPlugin.js' const monacoEditorPlugin = monacoEditorPluginCjs.default const nodeGlobalsPolyfillPlugin = nodeGlobalsPolyfillPluginCjs.default @@ -151,6 +152,7 @@ export function useTinyEngineBaseConfig(engineConfig) { const config = getDefaultConfig(engineConfig) config.plugins.push( + treeShakingPlugin(engineConfig.registryPath), createSvgIconsPlugin({ iconDirs: engineConfig.iconDirs || [], symbolId: 'icon-[name]', diff --git a/packages/build/vite-config/src/vite-plugins/treeShakingPlugin.js b/packages/build/vite-config/src/vite-plugins/treeShakingPlugin.js new file mode 100644 index 000000000..b0dea3439 --- /dev/null +++ b/packages/build/vite-config/src/vite-plugins/treeShakingPlugin.js @@ -0,0 +1,69 @@ +import fs from 'node:fs' +import path from 'node:path' +import replace from '@rollup/plugin-replace' +import { parse } from '@babel/parser' + +export function treeShakingPlugin(registryPath) { + if (!registryPath) { + return null + } + + const envReplace = {} + const filePath = path.resolve(process.cwd(), registryPath) + + if (!fs.existsSync(filePath)) { + return null + } + + try { + const fileContent = fs.readFileSync(filePath, 'utf-8') + const ast = parse(fileContent, { sourceType: 'module' }) + const commentPattern = /#__TINY_ENGINE_TREE_SHAKING__:\s*(?true|false)/ + + ast.program.body.forEach((item) => { + // 过滤默认导出 且导出类型为 object + if (item.type === 'ExportDefaultDeclaration' && item.declaration?.type === 'ObjectExpression') { + item.declaration.properties.forEach((propertyItem) => { + // 是属性 且 key 为 string + if (propertyItem.type === 'ObjectProperty' && propertyItem.key?.type === 'StringLiteral') { + const key = propertyItem.key.value + // 有 comment,解析 comment。 + // 通过 comment 指定 treeshaking,优先以 comment 为标准 + if (propertyItem.leadingComments?.length) { + for (const commentItem of propertyItem.leadingComments) { + const match = commentItem.value.match(commentPattern) + + if (!match) { + continue + } + + if (match.groups.value === 'true') { + envReplace[`__TINY_ENGINE_REMOVED_REGISTRY["${key}"]`] = false + } + + return + } + } + + // 注册表注释了插件,默认 tree-shaking, + if (propertyItem.value.type === 'BooleanLiteral' && propertyItem.value.value === false) { + envReplace[`__TINY_ENGINE_REMOVED_REGISTRY["${key}"]`] = false + } + } + }) + } + }) + + return replace({ + values: { + ...envReplace + }, + delimiters: ['', ''] + }) + } catch (error) { + const logger = console + logger.warn('[TinyEngine] tree-shaking plugin error', error) + } + + return null +} diff --git a/packages/build/vite-plugin-meta-comments/.gitignore b/packages/build/vite-plugin-meta-comments/.gitignore new file mode 100644 index 000000000..0b0136f51 --- /dev/null +++ b/packages/build/vite-plugin-meta-comments/.gitignore @@ -0,0 +1 @@ +test/temp diff --git a/packages/build/vite-plugin-meta-comments/index.js b/packages/build/vite-plugin-meta-comments/index.js index 3bb523313..1adfd8cb9 100644 --- a/packages/build/vite-plugin-meta-comments/index.js +++ b/packages/build/vite-plugin-meta-comments/index.js @@ -1,6 +1,6 @@ /** - * Copyright (c) 2024 - present TinyEngine Authors. - * Copyright (c) 2024 - present Huawei Cloud Computing Technologies Co., Ltd. + * Copyright (c) 2023 - present TinyEngine Authors. + * Copyright (c) 2023 - present Huawei Cloud Computing Technologies Co., Ltd. * * Use of this source code is governed by an MIT-style license. * diff --git a/packages/build/vite-plugin-meta-comments/package.json b/packages/build/vite-plugin-meta-comments/package.json index 9b89e182e..e3ffa151a 100644 --- a/packages/build/vite-plugin-meta-comments/package.json +++ b/packages/build/vite-plugin-meta-comments/package.json @@ -10,7 +10,9 @@ ], "scripts": { "build": "vite build", - "test": "node ./src/test/index.js" + "test": "vitest run", + "test:watch": "vitest", + "test:legacy": "node ./test/legacy/index.js" }, "devDependencies": { "@babel/generator": "^7.18.13", @@ -19,7 +21,8 @@ "@babel/traverse": "^7.18.13", "@vitejs/plugin-vue": "^5.1.2", "@vue/compiler-sfc": "^3.4.21", - "vite": "^5.4.2" + "vite": "^5.4.2", + "vitest": "^3.1.3" }, "keywords": [], "publishConfig": { diff --git a/packages/build/vite-plugin-meta-comments/src/test/code/output.js b/packages/build/vite-plugin-meta-comments/src/test/code/output.js deleted file mode 100644 index ff2bb833e..000000000 --- a/packages/build/vite-plugin-meta-comments/src/test/code/output.js +++ /dev/null @@ -1,598 +0,0 @@ -import { - callEntry as _callEntry, - beforeCallEntry as _beforeCallEntry, - afterCallEntry as _afterCallEntry, - useCompile as _useCompile -} from '@opentiny/tiny-engine-meta-register' -import _metaData from '../meta.js' -/* metaService */ -import { reactive, onMounted, onBeforeMount as beforeMount } from 'vue' -import { deepCopy } from 'loash-es' -export const useRenderless = _callEntry( - ({ props }) => { - const state = reactive({ - tableData: props.data || props.op.data || [] - }) - onMounted( - _callEntry(() => {}, { - metaData: { - id: `${_metaData.id}.onMounted[0]` - }, - ctx: () => { - let asyncVars = {} - try { - asyncVars = { - props, - state, - logMessage, - aaa, - bbb, - handleClick, - ccc, - sendMessage, - last, - reactive, - onMounted, - beforeMount, - deepCopy, - useRenderless - } - } catch (e) { - return { - reactive, - onMounted, - beforeMount, - deepCopy, - useRenderless - } - } - return asyncVars - } - }) - ) - onMounted( - _callEntry(() => {}, { - metaData: { - id: `${_metaData.id}.onMounted[1]` - }, - ctx: () => { - let asyncVars = {} - try { - asyncVars = { - props, - state, - logMessage, - aaa, - bbb, - handleClick, - ccc, - sendMessage, - last, - reactive, - onMounted, - beforeMount, - deepCopy, - useRenderless - } - } catch (e) { - return { - reactive, - onMounted, - beforeMount, - deepCopy, - useRenderless - } - } - return asyncVars - } - }) - ) - onMounted( - _callEntry(() => {}, { - metaData: { - id: `${_metaData.id}.onMounted[2]` - }, - ctx: () => { - let asyncVars = {} - try { - asyncVars = { - props, - state, - logMessage, - aaa, - bbb, - handleClick, - ccc, - sendMessage, - last, - reactive, - onMounted, - beforeMount, - deepCopy, - useRenderless - } - } catch (e) { - return { - reactive, - onMounted, - beforeMount, - deepCopy, - useRenderless - } - } - return asyncVars - } - }) - ) - beforeMount( - _callEntry(() => {}, { - metaData: { - id: `${_metaData.id}.onBeforeMount[0]` - }, - ctx: () => { - let asyncVars = {} - try { - asyncVars = { - props, - state, - logMessage, - aaa, - bbb, - handleClick, - ccc, - sendMessage, - last, - reactive, - onMounted, - beforeMount, - deepCopy, - useRenderless - } - } catch (e) { - return { - reactive, - onMounted, - beforeMount, - deepCopy, - useRenderless - } - } - return asyncVars - } - }) - ) - _beforeCallEntry({ - metaData: { - id: `${_metaData.id}.logMessage` - }, - ctx: () => { - let asyncVars = {} - try { - asyncVars = { - props, - state, - logMessage, - aaa, - bbb, - handleClick, - ccc, - sendMessage, - last, - reactive, - onMounted, - beforeMount, - deepCopy, - useRenderless - } - } catch (e) { - return { - props, - state, - last, - reactive, - onMounted, - beforeMount, - deepCopy, - useRenderless - } - } - return asyncVars - } - }) - const logMessage = _callEntry( - () => { - console.log('我是纯函数我不需要闭包参数') - }, - { - metaData: { - id: `${_metaData.id}.logMessage` - }, - ctx: () => { - let asyncVars = {} - try { - asyncVars = { - props, - state, - logMessage, - aaa, - bbb, - handleClick, - ccc, - sendMessage, - last, - reactive, - onMounted, - beforeMount, - deepCopy, - useRenderless - } - } catch (e) { - return { - props, - state, - last, - reactive, - onMounted, - beforeMount, - deepCopy, - useRenderless - } - } - return asyncVars - } - } - ) - _afterCallEntry({ - metaData: { - id: `${_metaData.id}.logMessage` - }, - ctx: () => { - let asyncVars = {} - try { - asyncVars = { - props, - state, - logMessage, - aaa, - bbb, - handleClick, - ccc, - sendMessage, - last, - reactive, - onMounted, - beforeMount, - deepCopy, - useRenderless - } - } catch (e) { - return { - props, - state, - last, - reactive, - onMounted, - beforeMount, - deepCopy, - useRenderless - } - } - return asyncVars - } - }) - const aaa = 'aaa', - bbb = 'bbb' - _beforeCallEntry({ - metaData: { - id: `${_metaData.id}.handleClick` - }, - ctx: () => { - let asyncVars = {} - try { - asyncVars = { - e, - props, - state, - logMessage, - aaa, - bbb, - handleClick, - ccc, - sendMessage, - last, - reactive, - onMounted, - beforeMount, - deepCopy, - useRenderless - } - } catch (e) { - return { - props, - state, - logMessage, - aaa, - bbb, - last, - reactive, - onMounted, - beforeMount, - deepCopy, - useRenderless - } - } - return asyncVars - } - }) - const handleClick = _callEntry( - (e) => { - console.log(e.target, aaa) - state.tableData.push({ - key: 'TinyEngine', - zhCN: '低代码引擎', - enUS: 'TinyEngine' - }) - }, - { - metaData: { - id: `${_metaData.id}.handleClick` - }, - ctx: () => { - let asyncVars = {} - try { - asyncVars = { - e, - props, - state, - logMessage, - aaa, - bbb, - handleClick, - ccc, - sendMessage, - last, - reactive, - onMounted, - beforeMount, - deepCopy, - useRenderless - } - } catch (e) { - return { - props, - state, - logMessage, - aaa, - bbb, - last, - reactive, - onMounted, - beforeMount, - deepCopy, - useRenderless - } - } - return asyncVars - } - } - ) - _afterCallEntry({ - metaData: { - id: `${_metaData.id}.handleClick` - }, - ctx: () => { - let asyncVars = {} - try { - asyncVars = { - e, - props, - state, - logMessage, - aaa, - bbb, - handleClick, - ccc, - sendMessage, - last, - reactive, - onMounted, - beforeMount, - deepCopy, - useRenderless - } - } catch (e) { - return { - props, - state, - logMessage, - aaa, - bbb, - last, - reactive, - onMounted, - beforeMount, - deepCopy, - useRenderless - } - } - return asyncVars - } - }) - const ccc = 111 - _beforeCallEntry({ - metaData: { - id: `${_metaData.id}.sendMessage` - }, - ctx: () => { - let asyncVars = {} - try { - asyncVars = { - props, - state, - logMessage, - aaa, - bbb, - handleClick, - ccc, - sendMessage, - last, - reactive, - onMounted, - beforeMount, - deepCopy, - useRenderless - } - } catch (e) { - return { - props, - state, - logMessage, - aaa, - bbb, - handleClick, - ccc, - last, - reactive, - onMounted, - beforeMount, - deepCopy, - useRenderless - } - } - return asyncVars - } - }) - const sendMessage = _callEntry( - () => { - logMessage('自定义是的范德萨') - }, - { - metaData: { - id: `${_metaData.id}.sendMessage` - }, - ctx: () => { - let asyncVars = {} - try { - asyncVars = { - props, - state, - logMessage, - aaa, - bbb, - handleClick, - ccc, - sendMessage, - last, - reactive, - onMounted, - beforeMount, - deepCopy, - useRenderless - } - } catch (e) { - return { - props, - state, - logMessage, - aaa, - bbb, - handleClick, - ccc, - last, - reactive, - onMounted, - beforeMount, - deepCopy, - useRenderless - } - } - return asyncVars - } - } - ) - _afterCallEntry({ - metaData: { - id: `${_metaData.id}.sendMessage` - }, - ctx: () => { - let asyncVars = {} - try { - asyncVars = { - props, - state, - logMessage, - aaa, - bbb, - handleClick, - ccc, - sendMessage, - last, - reactive, - onMounted, - beforeMount, - deepCopy, - useRenderless - } - } catch (e) { - return { - props, - state, - logMessage, - aaa, - bbb, - handleClick, - ccc, - last, - reactive, - onMounted, - beforeMount, - deepCopy, - useRenderless - } - } - return asyncVars - } - }) - function last() {} - return { - state, - aa, - handleClick, - sendMessage - } - }, - { - metaData: { - id: `${_metaData.id}.useRenderless` - }, - ctx: () => { - let asyncVars = {} - try { - asyncVars = { - props, - state, - logMessage, - aaa, - bbb, - handleClick, - ccc, - sendMessage, - last, - reactive, - onMounted, - beforeMount, - deepCopy, - useRenderless - } - } catch (e) { - return { - reactive, - onMounted, - beforeMount, - deepCopy - } - } - return asyncVars - } - } -) diff --git a/packages/build/vite-plugin-meta-comments/src/transform-sfc.js b/packages/build/vite-plugin-meta-comments/src/transform-sfc.js index bbb2568bd..6c396a664 100644 --- a/packages/build/vite-plugin-meta-comments/src/transform-sfc.js +++ b/packages/build/vite-plugin-meta-comments/src/transform-sfc.js @@ -1,6 +1,6 @@ /** - * Copyright (c) 2024 - present TinyEngine Authors. - * Copyright (c) 2024 - present Huawei Cloud Computing Technologies Co., Ltd. + * Copyright (c) 2023 - present TinyEngine Authors. + * Copyright (c) 2023 - present Huawei Cloud Computing Technologies Co., Ltd. * * Use of this source code is governed by an MIT-style license. * diff --git a/packages/build/vite-plugin-meta-comments/src/transform.js b/packages/build/vite-plugin-meta-comments/src/transform.js index 6ad8b6af4..8930ca670 100644 --- a/packages/build/vite-plugin-meta-comments/src/transform.js +++ b/packages/build/vite-plugin-meta-comments/src/transform.js @@ -1,6 +1,6 @@ /** - * Copyright (c) 2024 - present TinyEngine Authors. - * Copyright (c) 2024 - present Huawei Cloud Computing Technologies Co., Ltd. + * Copyright (c) 2023 - present TinyEngine Authors. + * Copyright (c) 2023 - present Huawei Cloud Computing Technologies Co., Ltd. * * Use of this source code is governed by an MIT-style license. * @@ -11,15 +11,13 @@ */ import { parse } from '@babel/parser' -import generate from '@babel/generator' -import traverse from '@babel/traverse' +import generateLib from '@babel/generator' +import traverseLib from '@babel/traverse' import template from '@babel/template' import { wrapEntryFuncNode, COMMON_PACKAGE_NAME, CALLENTRY, - BEFORE_CALLENTRY, - AFTER_CALLENTRY, USE_COMPILE, METADATANAME, isCallEntryFile, @@ -31,7 +29,9 @@ import { getModuleId } from './utils.js' -const generateTraverse = traverse.default +// 在ESM模式中,traverse默认是以命名导出的形式提供 +const traverse = traverseLib.default || traverseLib +const generate = generateLib.default || generateLib function handleFunctionExpression(state) { return function (path) { @@ -40,6 +40,7 @@ function handleFunctionExpression(state) { // 只有拿到函数的名称才可以被复写 if (functionName) { + state.ArrowOrFunctionExpression.push(functionName) wrapEntryFuncNode({ path, functionName, @@ -74,15 +75,26 @@ function handleImportDeclaration(state) { } } +/** + * 处理变量声明节点 + * @param {Object} state - 状态对象,用于存储变量声明信息 + * @returns {Function} 返回处理变量声明的函数 + */ function handleVariableDeclaration(state) { return function (path) { + // 遍历所有变量声明 path.node.declarations?.forEach((val) => { + // 获取变量名 const name = val.id.name + // 获取变量所在的作用域块 const block = path.scope.block + + // 如果该作用域块还没有记录过变量,则创建新数组 if (!state.varDeclartion.has(block)) { const arr = [name] state.varDeclartion.set(block, arr) } else { + // 如果该作用域块已存在,则将变量名添加到数组中 const arr = state.varDeclartion.get(block) arr.push(name) } @@ -127,19 +139,13 @@ function handleProgram(state, metaPath) { path.node.body.unshift(template.statement(`import ${metaData} from '${metaPath}'`)()) const callEntry = path.scope.generateUid(CALLENTRY) - const beforeCallEntry = path.scope.generateUid(BEFORE_CALLENTRY) - const afterCallEntry = path.scope.generateUid(AFTER_CALLENTRY) const useCompile = path.scope.generateUid(USE_COMPILE) state.varName[CALLENTRY] = callEntry - state.varName[BEFORE_CALLENTRY] = beforeCallEntry - state.varName[AFTER_CALLENTRY] = afterCallEntry state.varName[USE_COMPILE] = useCompile path.node.body.unshift( template.statement( `import { ${CALLENTRY} as ${callEntry}, - ${BEFORE_CALLENTRY} as ${beforeCallEntry}, - ${AFTER_CALLENTRY} as ${afterCallEntry}, ${USE_COMPILE} as ${useCompile} } from '${COMMON_PACKAGE_NAME}'` )() @@ -156,8 +162,7 @@ function handleExportDefaultDeclaration(state) { const lastComment = comment[comment.length - 1].value // 只判断最接近export default的注释节点 if (lastComment.includes('metaComponent')) { - wrapExportComp({ path, varName: state.varName }) - path.skip() + wrapExportComp({ path, varName: state.varName, lastComment }) } } } @@ -177,14 +182,16 @@ export const transform = (code, id) => { hooksIndex: {}, varDeclartion: new Map(), moduleId: '', // 自定义的模块ID,用于区分元服务中不同文件, - noUseVars: [] + noUseVars: [], + fileId: id, + ArrowOrFunctionExpression: [] } // 找不到meta.js告警并返回 const metaPath = getMeataPath(id) if (!metaPath) { // eslint-disable-next-line no-console - console.log('找不到对应的meta.js') + console.log(`${id}: 找不到对应的meta.js`) return } @@ -194,9 +201,33 @@ export const transform = (code, id) => { plugins: ['typescript', 'jsx'] }) - generateTraverse(resultAst, { + const handleCallExpression = (state) => (path) => { + const callee = path.node.callee + const name = callee.name + if (name === state.varName[CALLENTRY]) { + return + } + if (name) { + const bindings = path.scope.bindings + // 判断调用的函数是否来自本文件中的函数表达式,且在同一个函数作用域内 + if ( + callee.type === 'Identifier' && + bindings?.[name] && + bindings?.[name]?.kind !== 'module' && + state.ArrowOrFunctionExpression.includes(name) + ) { + // eslint-disable-next-line no-console + console.warn( + `文件 ${state.fileId} 中函数 ${name} 在声明后直接调用了,可能造成函数覆盖场景报错!整改建议:1、导入后再调用。2、在文件最后调用` + ) + } + } + } + + traverse(resultAst, { // 使用特定的类型回调处理、函数表达式、箭头函数、带导出的函数 'ArrowFunctionExpression|FunctionExpression': handleFunctionExpression(state), + CallExpression: handleCallExpression(state), ImportDeclaration: handleImportDeclaration(state), VariableDeclaration: handleVariableDeclaration(state), ExpressionStatement: handleExpressionStatement(state), @@ -204,5 +235,5 @@ export const transform = (code, id) => { ExportDefaultDeclaration: handleExportDefaultDeclaration(state) }) - return generate.default(resultAst).code || '' + return generate(resultAst).code || '' } diff --git a/packages/build/vite-plugin-meta-comments/src/utils.js b/packages/build/vite-plugin-meta-comments/src/utils.js index 4ea489ad0..7d82cfb0f 100644 --- a/packages/build/vite-plugin-meta-comments/src/utils.js +++ b/packages/build/vite-plugin-meta-comments/src/utils.js @@ -1,6 +1,6 @@ /** - * Copyright (c) 2024 - present TinyEngine Authors. - * Copyright (c) 2024 - present Huawei Cloud Computing Technologies Co., Ltd. + * Copyright (c) 2023 - present TinyEngine Authors. + * Copyright (c) 2023 - present Huawei Cloud Computing Technologies Co., Ltd. * * Use of this source code is governed by an MIT-style license. * @@ -14,12 +14,13 @@ import template from '@babel/template' import path from 'node:path' import fs from 'node:fs' -export const CALLENTRY = 'callEntry' -export const BEFORE_CALLENTRY = 'beforeCallEntry' -export const AFTER_CALLENTRY = 'afterCallEntry' -export const USE_COMPILE = 'useCompile' -export const METADATANAME = 'metaData' -export const COMMON_PACKAGE_NAME = '@opentiny/tiny-engine-meta-register' +// 定义各种常量,用于标识不同的入口点和编译选项 +export const CALLENTRY = 'callEntry' // 主入口点 +export const USE_COMPILE = 'useCompile' // 编译选项 +export const METADATANAME = 'metaData' // 元数据名称 +export const COMMON_PACKAGE_NAME = '@opentiny/tiny-engine-meta-register' // 公共包名 + +// Vue生命周期钩子列表 export const vueLifeHook = [ 'onMounted', 'onUpdated', @@ -30,41 +31,61 @@ export const vueLifeHook = [ 'onActivated', 'onDeactivated' ] + +// 用于匹配metaService和metaComponent注释的正则表达式 const callEntryExp = /\/\*\s*metaService/ const compileExp = /\/\*\s*metaComponent/ +// 创建Babel模板语句的辅助函数 const statement = (code) => template.statement(code, { placeholderPattern: false }) +/** + * 检查文件是否包含metaService注释 + * @param {string} code - 文件内容 + * @returns {boolean} 是否包含metaService注释 + */ export const isCallEntryFile = (code) => { - return callEntryExp.test(code) + return callEntryExp.test(code) || compileExp.test(code) } +/** + * 检查文件是否包含metaComponent注释 + * @param {string} code - 文件内容 + * @returns {boolean} 是否包含metaComponent注释 + */ export const isCompileFile = (code) => { return compileExp.test(code) } +/** + * 从注释中提取模块ID + * @param {string} str - 包含metaService注释的字符串 + * @returns {string} 提取的模块ID + */ export const getModuleId = (str) => { const [, moduleId = ''] = str.match(/\/\*\s*metaService: \s*(.+?)\s*\*\//) || [] return moduleId } -// 将注释中的参数提取出来,并组合成目前参数格式 -export const getEntryParam = ({ functionName = '', syncVars, asyncVars, state }) => { +export const getTemplateId = (str) => { + const [, moduleId = ''] = str.match(/\s*metaComponent: \s*(.+)\s*/) || [] + return moduleId.trim() +} + +/** + * 从注释中提取参数并组合成标准格式 + * @param {Object} params - 参数对象 + * @param {string} params.functionName - 函数名 + * @param {Object} params.asyncVars - 异步变量 + * @param {Object} params.state - 状态对象 + * @returns {string} 格式化后的参数 + */ +export const getEntryParam = ({ functionName = '', asyncVars, state }) => { const { varName, moduleId, noUseVars } = state const metaData = varName[METADATANAME] const id = moduleId ? `'${moduleId}.${functionName}'` : `\`\${${metaData}.id}.${functionName}\`` - const syncVarsKey = Object.keys(syncVars).filter((key) => !noUseVars.includes(key)) - const asyncVarsKey = Object.keys(asyncVars).filter((key) => !noUseVars.includes(key) && !syncVarsKey.includes(key)) - const ctx = ` () => { - let asyncVars = {} - const syncVars = {${syncVarsKey.join(',')}} - try { - asyncVars = { ${asyncVarsKey.join(',')} } - } catch { - return syncVars - } - return { ...syncVars, ...asyncVars } - }` + const asyncVarsKey = Object.keys(asyncVars).filter((key) => !noUseVars.includes(key)) + const ctx = `() => ({${asyncVarsKey.join(',')}})` if (functionName) { return `{ ${METADATANAME}: { id: ${id} }, ctx: ${ctx}}` } @@ -72,27 +93,12 @@ export const getEntryParam = ({ functionName = '', syncVars, asyncVars, state }) return `{ ${METADATANAME}: ${metaData} }` } -const getParentVariableDeclaration = (path) => { - if (!path) { - return - } - - if (path.type === 'VariableDeclaration' && path.parentPath.type !== 'ExportNamedDeclaration') { - return path - } else { - return getParentVariableDeclaration(path?.parentPath) - } -} - -const generateBeforeAfterEntry = ({ path, beforeEntryAst, afterEntryAst }) => { - const parent = getParentVariableDeclaration(path) - if (parent) { - parent.insertBefore(beforeEntryAst) - parent.insertAfter(afterEntryAst) - } -} - -export const getOuterBingdings = (path) => { +/** + * 获取外部绑定变量 + * @param {Object} path - Babel路径对象 + * @returns {Object} 外部绑定变量对象 + */ +export const getOuterBindings = (path) => { const outerBindings = {} const allBindings = path.scope.getAllBindings() const selfBindings = path.scope.bindings @@ -104,39 +110,11 @@ export const getOuterBingdings = (path) => { return outerBindings } -// 获取当前上下文已经可以使用的scope变量 -export const getValidBingdinngs = ({ path, state, functionName }) => { - const validBindings = {} - const { varDeclartion } = state - let varArr = [] - let parentPath = path.parentPath - let block - while (parentPath) { - const newBlock = parentPath.scope.block - parentPath = parentPath.parentPath - if (newBlock === block) { - continue - } - block = newBlock - varArr = varArr.concat(varDeclartion.get(block)) - } - - const allBindings = path.scope.getAllBindings() - const selfBindings = path.scope.bindings - Object.keys(allBindings).forEach((key) => { - if (selfBindings[key]) { - return - } - const value = allBindings[key] - // 如果是变量定义,并且此时还没有初始化,则过滤掉 - if ((['var', 'const', 'let'].includes(value.kind) && !varArr.includes(key)) || key === functionName) { - return - } - validBindings[key] = value - }) - return validBindings -} - +/** + * 获取模块级别的绑定变量 + * @param {Object} path - Babel路径对象 + * @returns {Object} 模块绑定变量对象 + */ export const getModuleBindings = (path) => { const moduleBindings = {} const allBindings = path.scope.getAllBindings() @@ -148,33 +126,37 @@ export const getModuleBindings = (path) => { return moduleBindings } -// 生成callEntry表达式并包裹当前函数,如果有参与还需要处理参数 +/** + * 生成callEntry表达式并包裹当前函数 + * @param {Object} params - 参数对象 + * @param {Object} params.path - Babel路径对象 + * @param {string} params.functionName - 函数名 + * @param {Object} params.varName - 变量名对象 + * @param {Object} params.state - 状态对象 + */ export const wrapEntryFuncNode = ({ path, functionName = '', varName, state }) => { - const syncVars = getValidBingdinngs({ path, state, functionName }) - const asyncVars = getOuterBingdings(path) + const asyncVars = getOuterBindings(path) const entryParam = getEntryParam({ functionName, - syncVars, asyncVars, varName, state }) const callEntry = varName[CALLENTRY] - const beforeCallEntry = varName[BEFORE_CALLENTRY] - const afterCallEntry = varName[AFTER_CALLENTRY] const entryAst = statement(`${callEntry}(${entryParam})`)() - const beforeEntryAst = statement(`${beforeCallEntry}(${entryParam})`)() - const afterEntryAst = statement(`${afterCallEntry}(${entryParam})`)() - const resultNode = path.node - generateBeforeAfterEntry({ path, beforeEntryAst, afterEntryAst }) entryAst.expression.arguments.unshift(JSON.parse(JSON.stringify(resultNode))) // 替换整个节点 path.replaceWith(entryAst) } -// 获取两个文件路径的相对路径,入参为两个文件绝对路径 +/** + * 获取两个文件路径的相对路径 + * @param {string} path1 - 第一个文件的绝对路径 + * @param {string} path2 - 第二个文件的绝对路径 + * @returns {string} 相对路径 + */ export const getRelFilePath = (path1, path2) => { const dir1 = path.join(path1, '..') const dir2 = path.join(path2, '..') @@ -182,7 +164,11 @@ export const getRelFilePath = (path1, path2) => { return `${relPath}/${path.basename(path2)}`.replaceAll('\\', '/') } -// 向上获取meta.js的相对路径 +/** + * 向上查找meta.js的相对路径 + * @param {string} id - 文件ID + * @returns {string|null} meta.js的相对路径或null + */ export const getMeataPath = (id) => { let tempPath = path.join(id, '../meta.js') @@ -205,38 +191,99 @@ export const getMeataPath = (id) => { return null } -export const wrapExportComp = ({ path, varName }) => { +/** + * 包装导出组件 + * @param {Object} params - 参数对象 + * @param {Object} params.path - Babel路径对象 + * @param {Object} params.varName - 变量名对象 + */ +/** + * 包装导出组件,对组件进行useCompile包装处理 + * @param {Object} params - 参数对象 + * @param {Object} params.path - Babel路径对象,用于AST操作 + * @param {Object} params.varName - 变量名对象,包含元数据和编译函数名 + * @param {string} params.lastComment - 最后一个注释,用于生成模板ID + */ +export const wrapExportComp = ({ path, varName, lastComment }) => { + // 获取导出对象的所有属性 const properties = path.node.declaration?.properties || [] + // 获取元数据变量名 const metaData = varName[METADATANAME] + // 获取useCompile函数名 const useCompile = varName[USE_COMPILE] + // 从注释中获取模板ID + const templateId = getTemplateId(lastComment) - // 对键值为component属性包一层useCompile + // 遍历导出对象的所有属性 properties.forEach((prop) => { + // 处理单个组件导出的情况 if (prop.key?.name === 'component') { + // 获取组件值 const val = prop.value + // 创建useCompile包装的AST节点 const compileAst = statement(`${useCompile}({ component: null, ${METADATANAME}: ${metaData} });`)() + // 将原组件值设置到useCompile的component参数中 compileAst.expression.arguments[0].properties[0].value = val + // 遍历AST,替换原组件节点 path.traverse({ enter(subPath) { if (subPath.node === val) { + // 用useCompile包装后的节点替换原节点 subPath.replaceWith(compileAst) + // 跳过子节点遍历 subPath.skip() } } }) } + // 处理多个组件导出的情况 + else if (prop.key?.name === 'components') { + const val = prop.value + // 确保components是一个对象 + if (val?.properties) { + const properties = val.properties + // 遍历所有子组件 + properties.forEach((item) => { + const value = item.value + // 生成元数据对象,如果有templateId则使用"templateId.组件名"作为id + const metaObj = templateId ? `{id:'${templateId}.${value.name}'}` : metaData + // 创建useCompile包装的AST节点 + const compileAst = statement(`${useCompile}({ component: null, ${METADATANAME}: ${metaObj} });`)() + // 将原组件值设置到useCompile的component参数中 + compileAst.expression.arguments[0].properties[0].value = value + // 遍历AST,替换原组件节点 + path.traverse({ + enter(subPath) { + if (subPath.node === value) { + // 用useCompile包装后的节点替换原节点 + subPath.replaceWith(compileAst) + // 跳过子节点遍历 + subPath.skip() + } + } + }) + }) + } + } }) } +/** + * 包装钩子调用 + * @param {Object} params - 参数对象 + * @param {Object} params.path - Babel路径对象 + * @param {Object} params.varName - 变量名对象 + * @param {string} params.functionName - 函数名 + * @param {string} params.callName - 调用名 + * @param {Object} params.state - 状态对象 + */ export const wrapHookCall = ({ path, varName, functionName, callName, state }) => { // vue的生命周期hook只有一个参数 const argument = path.node.expression.arguments[0] const callEntry = varName[CALLENTRY] - const syncVars = getValidBingdinngs({ path, state, functionName }) const asyncVars = path.scope.getAllBindings() const entryParam = getEntryParam({ functionName, - syncVars, asyncVars, varName, state diff --git a/packages/build/vite-plugin-meta-comments/test/expected/arrow-func.js.output.js b/packages/build/vite-plugin-meta-comments/test/expected/arrow-func.js.output.js new file mode 100644 index 000000000..1f7675e60 --- /dev/null +++ b/packages/build/vite-plugin-meta-comments/test/expected/arrow-func.js.output.js @@ -0,0 +1,15 @@ +import { callEntry as _callEntry, useCompile as _useCompile } from '@opentiny/tiny-engine-meta-register'; +import _metaData from './meta.js'; +/* metaService */ +const arrowFunc = _callEntry(() => { + console.log('Arrow function'); + return true; +}, { + metaData: { + id: `${_metaData.id}.arrowFunc` + }, + ctx: () => ({ + arrowFunc + }) +}); +export { arrowFunc }; \ No newline at end of file diff --git a/packages/build/vite-plugin-meta-comments/test/expected/entry.js.output.js b/packages/build/vite-plugin-meta-comments/test/expected/entry.js.output.js new file mode 100644 index 000000000..ad7eb7782 --- /dev/null +++ b/packages/build/vite-plugin-meta-comments/test/expected/entry.js.output.js @@ -0,0 +1,124 @@ +import { callEntry as _callEntry, useCompile as _useCompile } from '@opentiny/tiny-engine-meta-register'; +import _metaData from './meta.js'; +/* metaService */ +/** + * Copyright (c) 2023 - present TinyEngine Authors. + * Copyright (c) 2023 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ + +import { reactive, onMounted, onBeforeMount as beforeMount } from 'vue'; +import { deepCopy } from 'lodash-es'; +export const useRenderless = _callEntry(({ + props +}) => { + const state = reactive({ + tableData: props.data || props.op.data || [] + }); + const last1 = useLayout(last1); + beforeMount(_callEntry(() => {}, { + metaData: { + id: `${_metaData.id}.onBeforeMount[0]` + }, + ctx: () => ({ + props, + state, + last1, + logMessage, + aaa, + bbb, + handleClick, + ccc, + sendMessage, + deepCopy, + useRenderless + }) + })); + const logMessage = _callEntry(() => { + console.log('我是纯函数我不需要闭包参数'); + }, { + metaData: { + id: `${_metaData.id}.logMessage` + }, + ctx: () => ({ + props, + state, + last1, + logMessage, + aaa, + bbb, + handleClick, + ccc, + sendMessage, + deepCopy, + useRenderless + }) + }); + const aaa = 'aaa', + bbb = 'bbb'; + const handleClick = _callEntry(e => { + state.tableData.push({ + key: 'TinyEngine', + zhCN: '低代码引擎', + enUS: 'TinyEngine' + }); + }, { + metaData: { + id: `${_metaData.id}.handleClick` + }, + ctx: () => ({ + props, + state, + last1, + logMessage, + aaa, + bbb, + handleClick, + ccc, + sendMessage, + deepCopy, + useRenderless + }) + }); + const ccc = 111; + const sendMessage = _callEntry(() => { + logMessage('自定义是的范德萨'); + }, { + metaData: { + id: `${_metaData.id}.sendMessage` + }, + ctx: () => ({ + props, + state, + last1, + logMessage, + aaa, + bbb, + handleClick, + ccc, + sendMessage, + deepCopy, + useRenderless + }) + }); + return { + state, + aa, + handleClick, + sendMessage + }; +}, { + metaData: { + id: `${_metaData.id}.useRenderless` + }, + ctx: () => ({ + deepCopy, + useRenderless + }) +}); \ No newline at end of file diff --git a/packages/build/vite-plugin-meta-comments/test/expected/function-expr.js.output.js b/packages/build/vite-plugin-meta-comments/test/expected/function-expr.js.output.js new file mode 100644 index 000000000..8839116aa --- /dev/null +++ b/packages/build/vite-plugin-meta-comments/test/expected/function-expr.js.output.js @@ -0,0 +1,15 @@ +import { callEntry as _callEntry, useCompile as _useCompile } from '@opentiny/tiny-engine-meta-register'; +import _metaData from './meta.js'; +/* metaService */ +const doSomething = _callEntry(function () { + console.log('Doing something'); + return true; +}, { + metaData: { + id: `${_metaData.id}.doSomething` + }, + ctx: () => ({ + doSomething + }) +}); +export { doSomething }; \ No newline at end of file diff --git a/packages/build/vite-plugin-meta-comments/test/expected/meta-service.js.output.js b/packages/build/vite-plugin-meta-comments/test/expected/meta-service.js.output.js new file mode 100644 index 000000000..e5c0d8b1b --- /dev/null +++ b/packages/build/vite-plugin-meta-comments/test/expected/meta-service.js.output.js @@ -0,0 +1,32 @@ +import { callEntry as _callEntry, useCompile as _useCompile } from '@opentiny/tiny-engine-meta-register'; +import _metaData from './meta.js'; +/* metaService: testModule */ +import { reactive } from 'vue'; +export const useTestService = _callEntry(() => { + const state = reactive({ + count: 0 + }); + const increment = _callEntry(() => { + state.count++; + }, { + metaData: { + id: 'testModule.increment' + }, + ctx: () => ({ + state, + increment, + useTestService + }) + }); + return { + state, + increment + }; +}, { + metaData: { + id: 'testModule.useTestService' + }, + ctx: () => ({ + useTestService + }) +}); \ No newline at end of file diff --git a/packages/build/vite-plugin-meta-comments/test/expected/meta-service.vue.output.vue b/packages/build/vite-plugin-meta-comments/test/expected/meta-service.vue.output.vue new file mode 100644 index 000000000..90381799d --- /dev/null +++ b/packages/build/vite-plugin-meta-comments/test/expected/meta-service.vue.output.vue @@ -0,0 +1,42 @@ + + + + diff --git a/packages/build/vite-plugin-meta-comments/test/expected/script-setup.vue.output.vue b/packages/build/vite-plugin-meta-comments/test/expected/script-setup.vue.output.vue new file mode 100644 index 000000000..e4550461d --- /dev/null +++ b/packages/build/vite-plugin-meta-comments/test/expected/script-setup.vue.output.vue @@ -0,0 +1,30 @@ + + + + diff --git a/packages/build/vite-plugin-meta-comments/test/expected/vue-hooks.js.output.js b/packages/build/vite-plugin-meta-comments/test/expected/vue-hooks.js.output.js new file mode 100644 index 000000000..eea2de5b1 --- /dev/null +++ b/packages/build/vite-plugin-meta-comments/test/expected/vue-hooks.js.output.js @@ -0,0 +1,33 @@ +import { callEntry as _callEntry, useCompile as _useCompile } from '@opentiny/tiny-engine-meta-register'; +import _metaData from './meta.js'; +/* metaService */ +import { onMounted, onBeforeMount } from 'vue'; +export const useHooks = _callEntry(() => { + onMounted(_callEntry(() => { + console.log('Component mounted'); + }, { + metaData: { + id: `${_metaData.id}.onMounted[0]` + }, + ctx: () => ({ + useHooks + }) + })); + onBeforeMount(_callEntry(() => { + console.log('Component will mount'); + }, { + metaData: { + id: `${_metaData.id}.onBeforeMount[0]` + }, + ctx: () => ({ + useHooks + }) + })); +}, { + metaData: { + id: `${_metaData.id}.useHooks` + }, + ctx: () => ({ + useHooks + }) +}); \ No newline at end of file diff --git a/packages/build/vite-plugin-meta-comments/src/test/code/entry.js b/packages/build/vite-plugin-meta-comments/test/legacy/code/entry.js similarity index 79% rename from packages/build/vite-plugin-meta-comments/src/test/code/entry.js rename to packages/build/vite-plugin-meta-comments/test/legacy/code/entry.js index 33c83d150..c96077be9 100644 --- a/packages/build/vite-plugin-meta-comments/src/test/code/entry.js +++ b/packages/build/vite-plugin-meta-comments/test/legacy/code/entry.js @@ -1,7 +1,7 @@ /* metaService */ /** - * Copyright (c) 2024 - present TinyEngine Authors. - * Copyright (c) 2024 - present Huawei Cloud Computing Technologies Co., Ltd. + * Copyright (c) 2023 - present TinyEngine Authors. + * Copyright (c) 2023 - present Huawei Cloud Computing Technologies Co., Ltd. * * Use of this source code is governed by an MIT-style license. * @@ -12,15 +12,13 @@ */ import { reactive, onMounted, onBeforeMount as beforeMount } from 'vue' -import { deepCopy } from 'loash-es' +import { deepCopy } from 'lodash-es' export const useRenderless = ({ props }) => { const state = reactive({ tableData: props.data || props.op.data || [] }) - onMounted(() => {}) - onMounted(() => {}) - onMounted(() => {}) + const last1 = useLayout(last1) beforeMount(() => {}) @@ -32,7 +30,6 @@ export const useRenderless = ({ props }) => { bbb = 'bbb' const handleClick = (e) => { - console.log(e.target, aaa) state.tableData.push({ key: 'TinyEngine', zhCN: '低代码引擎', @@ -46,8 +43,6 @@ export const useRenderless = ({ props }) => { logMessage('自定义是的范德萨') } - function last() {} - return { state, aa, diff --git a/packages/build/vite-plugin-meta-comments/test/legacy/code/output.js b/packages/build/vite-plugin-meta-comments/test/legacy/code/output.js new file mode 100644 index 000000000..610fc7ca2 --- /dev/null +++ b/packages/build/vite-plugin-meta-comments/test/legacy/code/output.js @@ -0,0 +1,136 @@ +import { callEntry as _callEntry, useCompile as _useCompile } from '@opentiny/tiny-engine-meta-register' +import _metaData from '../meta.js' +/* metaService */ +/** + * Copyright (c) 2023 - present TinyEngine Authors. + * Copyright (c) 2023 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ + +import { reactive, onMounted, onBeforeMount as beforeMount } from 'vue' +import { deepCopy } from 'lodash-es' +export const useRenderless = _callEntry( + ({ props }) => { + const state = reactive({ + tableData: props.data || props.op.data || [] + }) + const last1 = useLayout(last1) + beforeMount( + _callEntry(() => {}, { + metaData: { + id: `${_metaData.id}.onBeforeMount[0]` + }, + ctx: () => ({ + props, + state, + last1, + logMessage, + aaa, + bbb, + handleClick, + ccc, + sendMessage, + deepCopy, + useRenderless + }) + }) + ) + const logMessage = _callEntry( + () => { + console.log('我是纯函数我不需要闭包参数') + }, + { + metaData: { + id: `${_metaData.id}.logMessage` + }, + ctx: () => ({ + props, + state, + last1, + logMessage, + aaa, + bbb, + handleClick, + ccc, + sendMessage, + deepCopy, + useRenderless + }) + } + ) + const aaa = 'aaa', + bbb = 'bbb' + const handleClick = _callEntry( + (e) => { + state.tableData.push({ + key: 'TinyEngine', + zhCN: '低代码引擎', + enUS: 'TinyEngine' + }) + }, + { + metaData: { + id: `${_metaData.id}.handleClick` + }, + ctx: () => ({ + props, + state, + last1, + logMessage, + aaa, + bbb, + handleClick, + ccc, + sendMessage, + deepCopy, + useRenderless + }) + } + ) + const ccc = 111 + const sendMessage = _callEntry( + () => { + logMessage('自定义是的范德萨') + }, + { + metaData: { + id: `${_metaData.id}.sendMessage` + }, + ctx: () => ({ + props, + state, + last1, + logMessage, + aaa, + bbb, + handleClick, + ccc, + sendMessage, + deepCopy, + useRenderless + }) + } + ) + return { + state, + aa, + handleClick, + sendMessage + } + }, + { + metaData: { + id: `${_metaData.id}.useRenderless` + }, + ctx: () => ({ + deepCopy, + useRenderless + }) + } +) diff --git a/packages/build/vite-plugin-meta-comments/src/test/index.js b/packages/build/vite-plugin-meta-comments/test/legacy/index.js similarity index 82% rename from packages/build/vite-plugin-meta-comments/src/test/index.js rename to packages/build/vite-plugin-meta-comments/test/legacy/index.js index 981bc5d90..a1dcb0944 100644 --- a/packages/build/vite-plugin-meta-comments/src/test/index.js +++ b/packages/build/vite-plugin-meta-comments/test/legacy/index.js @@ -1,6 +1,6 @@ /** - * Copyright (c) 2024 - present TinyEngine Authors. - * Copyright (c) 2024 - present Huawei Cloud Computing Technologies Co., Ltd. + * Copyright (c) 2023 - present TinyEngine Authors. + * Copyright (c) 2023 - present Huawei Cloud Computing Technologies Co., Ltd. * * Use of this source code is governed by an MIT-style license. * @@ -11,7 +11,7 @@ */ import fs from 'fs' -import { transform } from '../transform.js' +import { transform } from '../../src/transform.js' import { fileURLToPath } from 'node:url' import * as path from 'path' diff --git a/packages/build/vite-plugin-meta-comments/src/test/meta.js b/packages/build/vite-plugin-meta-comments/test/legacy/meta.js similarity index 100% rename from packages/build/vite-plugin-meta-comments/src/test/meta.js rename to packages/build/vite-plugin-meta-comments/test/legacy/meta.js diff --git a/packages/build/vite-plugin-meta-comments/test/transform-sfc.test.js b/packages/build/vite-plugin-meta-comments/test/transform-sfc.test.js new file mode 100644 index 000000000..b5919b644 --- /dev/null +++ b/packages/build/vite-plugin-meta-comments/test/transform-sfc.test.js @@ -0,0 +1,195 @@ +/** + * Copyright (c) 2023 - present TinyEngine Authors. + * Copyright (c) 2023 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ + +import fs from 'node:fs' +import path from 'node:path' +import { fileURLToPath } from 'node:url' +import { describe, it, expect, beforeAll } from 'vitest' +import { transformSFC } from '../src/transform-sfc.js' + +// 获取当前文件的目录 +const __filename = fileURLToPath(import.meta.url) +const __dirname = path.dirname(__filename) + +// 确保测试目录存在并创建meta.js +beforeAll(() => { + const testDir = path.join(__dirname, 'temp/transform-sfc-test-cases') + if (!fs.existsSync(testDir)) { + fs.mkdirSync(testDir, { recursive: true }) + } + + // 创建meta.js文件用于测试 + const metaJsContent = ` +export default { + id: 'test-meta', + name: '测试元数据' +} +` + const metaJsPath = path.join(__dirname, 'temp/transform-sfc-test-cases/meta.js') + fs.writeFileSync(metaJsPath, metaJsContent, 'utf8') +}) + +// 创建测试用例文件的辅助函数 +const createTestFile = (filename, content) => { + const filePath = path.join(__dirname, 'temp/transform-sfc-test-cases', filename) + fs.writeFileSync(filePath, content, 'utf8') + return filePath +} + +// 执行转换并验证结果 +const testTransformAndVerify = async (fileName, expectTransformed = true) => { + const filePath = path.join(__dirname, 'temp/transform-sfc-test-cases', fileName) + const code = fs.readFileSync(filePath, 'utf8') + + const result = transformSFC(code, filePath) + + if (expectTransformed) { + // 确保转换结果不为空 + expect(result).toBeTruthy() + expect(result.length).toBeGreaterThan(0) + await expect(result).toMatchFileSnapshot(`./expected/${fileName}.output.vue`) + } else { + // 对于不应被转换的文件,transformSFC应返回undefined + expect(result).toBeUndefined() + } + + return result +} + +describe('transform-sfc.js', () => { + describe('转换包含metaService注释的Vue SFC', () => { + it('应该正确转换单文件组件并添加callEntry调用', async () => { + // 创建包含metaService注释的Vue SFC文件 + const content = ` + + + +` + + createTestFile('meta-service.vue', content) + + // 执行转换 + const result = await testTransformAndVerify('meta-service.vue') + + // 检查转换结果是否包含callEntry调用 + expect(result).toMatch(/callEntry\(/) + + // 检查转换结果是否保留了原始的template和style标签 + expect(result).toMatch(/