From 80c6d65cac994a8b477ed2db03be323db2f542fa Mon Sep 17 00:00:00 2001 From: lixuan Date: Mon, 9 Mar 2026 20:15:02 -0700 Subject: [PATCH 1/9] feat: introduce vue-to-dsl plugin (#1590) --- .../src/vite-plugins/devAliasPlugin.js | 3 +- packages/toolbars/generate-code/package.json | 3 +- packages/toolbars/generate-code/src/Main.vue | 452 ++- .../generate-code/src/OverwriteDialog.vue | 157 ++ packages/vue-to-dsl/.gitignore | 1 + packages/vue-to-dsl/README.md | 224 ++ packages/vue-to-dsl/cli.ts | 203 ++ packages/vue-to-dsl/package.json | 55 + packages/vue-to-dsl/src/constants.ts | 217 ++ packages/vue-to-dsl/src/converter.ts | 825 ++++++ packages/vue-to-dsl/src/generator/index.ts | 173 ++ packages/vue-to-dsl/src/index.d.ts | 155 + packages/vue-to-dsl/src/index.ts | 7 + packages/vue-to-dsl/src/parser/index.ts | 63 + packages/vue-to-dsl/src/parsers/index.ts | 3 + .../vue-to-dsl/src/parsers/scriptParser.ts | 488 ++++ .../vue-to-dsl/src/parsers/styleParser.ts | 77 + .../vue-to-dsl/src/parsers/templateParser.ts | 276 ++ packages/vue-to-dsl/src/shims.d.ts | 0 packages/vue-to-dsl/src/types.ts | 0 packages/vue-to-dsl/src/types/index.js | 73 + .../test/full/expected/schema.from-zip.json | 2484 +++++++++++++++++ .../vue-to-dsl/test/full/expected/schema.json | 2484 +++++++++++++++++ packages/vue-to-dsl/test/full/index.test.js | 43 + .../vue-to-dsl/test/full/input/appdemo01.zip | Bin 0 -> 37337 bytes .../test/full/input/appdemo01/.gitignore | 13 + .../test/full/input/appdemo01/README.md | 19 + .../test/full/input/appdemo01/index.html | 13 + .../test/full/input/appdemo01/package.json | 29 + .../full/input/appdemo01/public/favicon.ico | Bin 0 -> 12996 bytes .../test/full/input/appdemo01/src/App.vue | 11 + .../full/input/appdemo01/src/http/axios.js | 143 + .../full/input/appdemo01/src/http/config.js | 15 + .../full/input/appdemo01/src/http/index.js | 27 + .../full/input/appdemo01/src/i18n/en_US.json | 25 + .../full/input/appdemo01/src/i18n/index.js | 9 + .../full/input/appdemo01/src/i18n/locale.js | 4 + .../full/input/appdemo01/src/i18n/zh_CN.json | 26 + .../appdemo01/src/lowcodeConfig/bridge.js | 13 + .../appdemo01/src/lowcodeConfig/dataSource.js | 104 + .../src/lowcodeConfig/dataSource.json | 632 +++++ .../appdemo01/src/lowcodeConfig/lowcode.js | 86 + .../appdemo01/src/lowcodeConfig/store.js | 13 + .../test/full/input/appdemo01/src/main.js | 20 + .../full/input/appdemo01/src/router/index.js | 37 + .../full/input/appdemo01/src/stores/index.js | 1 + .../input/appdemo01/src/stores/testState.js | 27 + .../test/full/input/appdemo01/src/utils.js | 13 + .../input/appdemo01/src/views/DemoPage.vue | 27 + .../appdemo01/src/views/LifeCyclePage.vue | 60 + .../input/appdemo01/src/views/UntitledA.vue | 38 + .../input/appdemo01/src/views/createVm.vue | 442 +++ .../test/full/input/appdemo01/vite.config.js | 24 + .../vue-to-dsl/test/sfc/converter.test.js | 123 + .../testcases/001_simple/expected/schema.json | 60 + .../testcases/001_simple/input/component.vue | 49 + .../002_createVM/expected/schema.json | 1125 ++++++++ .../002_createVM/input/component.vue | 411 +++ .../testcases/003_login/expected/schema.json | 254 ++ .../testcases/003_login/input/component.vue | 106 + .../004_dashboard/expected/schema.json | 408 +++ .../004_dashboard/input/component.vue | 141 + .../testcases/005_survey/expected/schema.json | 223 ++ .../testcases/005_survey/input/component.vue | 85 + .../006_lifecycle/expected/schema.json | 64 + .../006_lifecycle/input/component.vue | 54 + .../vue-to-dsl/test/testcases/index.test.js | 98 + packages/vue-to-dsl/tsconfig.json | 20 + packages/vue-to-dsl/vite.config.cli.mjs | 32 + packages/vue-to-dsl/vite.config.js | 41 + 70 files changed, 13623 insertions(+), 8 deletions(-) create mode 100644 packages/toolbars/generate-code/src/OverwriteDialog.vue create mode 100644 packages/vue-to-dsl/.gitignore create mode 100644 packages/vue-to-dsl/README.md create mode 100644 packages/vue-to-dsl/cli.ts create mode 100644 packages/vue-to-dsl/package.json create mode 100644 packages/vue-to-dsl/src/constants.ts create mode 100644 packages/vue-to-dsl/src/converter.ts create mode 100644 packages/vue-to-dsl/src/generator/index.ts create mode 100644 packages/vue-to-dsl/src/index.d.ts create mode 100644 packages/vue-to-dsl/src/index.ts create mode 100644 packages/vue-to-dsl/src/parser/index.ts create mode 100644 packages/vue-to-dsl/src/parsers/index.ts create mode 100644 packages/vue-to-dsl/src/parsers/scriptParser.ts create mode 100644 packages/vue-to-dsl/src/parsers/styleParser.ts create mode 100644 packages/vue-to-dsl/src/parsers/templateParser.ts create mode 100644 packages/vue-to-dsl/src/shims.d.ts create mode 100644 packages/vue-to-dsl/src/types.ts create mode 100644 packages/vue-to-dsl/src/types/index.js create mode 100644 packages/vue-to-dsl/test/full/expected/schema.from-zip.json create mode 100644 packages/vue-to-dsl/test/full/expected/schema.json create mode 100644 packages/vue-to-dsl/test/full/index.test.js create mode 100644 packages/vue-to-dsl/test/full/input/appdemo01.zip create mode 100644 packages/vue-to-dsl/test/full/input/appdemo01/.gitignore create mode 100644 packages/vue-to-dsl/test/full/input/appdemo01/README.md create mode 100644 packages/vue-to-dsl/test/full/input/appdemo01/index.html create mode 100644 packages/vue-to-dsl/test/full/input/appdemo01/package.json create mode 100644 packages/vue-to-dsl/test/full/input/appdemo01/public/favicon.ico create mode 100644 packages/vue-to-dsl/test/full/input/appdemo01/src/App.vue create mode 100644 packages/vue-to-dsl/test/full/input/appdemo01/src/http/axios.js create mode 100644 packages/vue-to-dsl/test/full/input/appdemo01/src/http/config.js create mode 100644 packages/vue-to-dsl/test/full/input/appdemo01/src/http/index.js create mode 100644 packages/vue-to-dsl/test/full/input/appdemo01/src/i18n/en_US.json create mode 100644 packages/vue-to-dsl/test/full/input/appdemo01/src/i18n/index.js create mode 100644 packages/vue-to-dsl/test/full/input/appdemo01/src/i18n/locale.js create mode 100644 packages/vue-to-dsl/test/full/input/appdemo01/src/i18n/zh_CN.json create mode 100644 packages/vue-to-dsl/test/full/input/appdemo01/src/lowcodeConfig/bridge.js create mode 100644 packages/vue-to-dsl/test/full/input/appdemo01/src/lowcodeConfig/dataSource.js create mode 100644 packages/vue-to-dsl/test/full/input/appdemo01/src/lowcodeConfig/dataSource.json create mode 100644 packages/vue-to-dsl/test/full/input/appdemo01/src/lowcodeConfig/lowcode.js create mode 100644 packages/vue-to-dsl/test/full/input/appdemo01/src/lowcodeConfig/store.js create mode 100644 packages/vue-to-dsl/test/full/input/appdemo01/src/main.js create mode 100644 packages/vue-to-dsl/test/full/input/appdemo01/src/router/index.js create mode 100644 packages/vue-to-dsl/test/full/input/appdemo01/src/stores/index.js create mode 100644 packages/vue-to-dsl/test/full/input/appdemo01/src/stores/testState.js create mode 100644 packages/vue-to-dsl/test/full/input/appdemo01/src/utils.js create mode 100644 packages/vue-to-dsl/test/full/input/appdemo01/src/views/DemoPage.vue create mode 100644 packages/vue-to-dsl/test/full/input/appdemo01/src/views/LifeCyclePage.vue create mode 100644 packages/vue-to-dsl/test/full/input/appdemo01/src/views/UntitledA.vue create mode 100644 packages/vue-to-dsl/test/full/input/appdemo01/src/views/createVm.vue create mode 100644 packages/vue-to-dsl/test/full/input/appdemo01/vite.config.js create mode 100644 packages/vue-to-dsl/test/sfc/converter.test.js create mode 100644 packages/vue-to-dsl/test/testcases/001_simple/expected/schema.json create mode 100644 packages/vue-to-dsl/test/testcases/001_simple/input/component.vue create mode 100644 packages/vue-to-dsl/test/testcases/002_createVM/expected/schema.json create mode 100644 packages/vue-to-dsl/test/testcases/002_createVM/input/component.vue create mode 100644 packages/vue-to-dsl/test/testcases/003_login/expected/schema.json create mode 100644 packages/vue-to-dsl/test/testcases/003_login/input/component.vue create mode 100644 packages/vue-to-dsl/test/testcases/004_dashboard/expected/schema.json create mode 100644 packages/vue-to-dsl/test/testcases/004_dashboard/input/component.vue create mode 100644 packages/vue-to-dsl/test/testcases/005_survey/expected/schema.json create mode 100644 packages/vue-to-dsl/test/testcases/005_survey/input/component.vue create mode 100644 packages/vue-to-dsl/test/testcases/006_lifecycle/expected/schema.json create mode 100644 packages/vue-to-dsl/test/testcases/006_lifecycle/input/component.vue create mode 100644 packages/vue-to-dsl/test/testcases/index.test.js create mode 100644 packages/vue-to-dsl/tsconfig.json create mode 100644 packages/vue-to-dsl/vite.config.cli.mjs create mode 100644 packages/vue-to-dsl/vite.config.js diff --git a/packages/build/vite-config/src/vite-plugins/devAliasPlugin.js b/packages/build/vite-config/src/vite-plugins/devAliasPlugin.js index d914af40b4..94392e4872 100644 --- a/packages/build/vite-config/src/vite-plugins/devAliasPlugin.js +++ b/packages/build/vite-config/src/vite-plugins/devAliasPlugin.js @@ -70,7 +70,8 @@ const getDevAlias = (useSourceAlias) => { '@opentiny/tiny-engine-workspace-template-center': path.resolve( basePath, 'packages/workspace/template-center/index.ts' - ) + ), + '@opentiny/tiny-engine-vue-to-dsl': path.resolve(basePath, 'packages/vue-to-dsl/src/index.ts') } } diff --git a/packages/toolbars/generate-code/package.json b/packages/toolbars/generate-code/package.json index ecb7000197..063660a186 100644 --- a/packages/toolbars/generate-code/package.json +++ b/packages/toolbars/generate-code/package.json @@ -27,7 +27,8 @@ "dependencies": { "@opentiny/tiny-engine-common": "workspace:*", "@opentiny/tiny-engine-meta-register": "workspace:*", - "@opentiny/tiny-engine-utils": "workspace:*" + "@opentiny/tiny-engine-utils": "workspace:*", + "@opentiny/tiny-engine-vue-to-dsl": "workspace:*" }, "devDependencies": { "@opentiny/tiny-engine-vite-plugin-meta-comments": "workspace:*", diff --git a/packages/toolbars/generate-code/src/Main.vue b/packages/toolbars/generate-code/src/Main.vue index 245ab6c429..9bb2cde6fa 100644 --- a/packages/toolbars/generate-code/src/Main.vue +++ b/packages/toolbars/generate-code/src/Main.vue @@ -1,6 +1,6 @@ diff --git a/packages/vue-to-dsl/.gitignore b/packages/vue-to-dsl/.gitignore new file mode 100644 index 0000000000..9b1960e711 --- /dev/null +++ b/packages/vue-to-dsl/.gitignore @@ -0,0 +1 @@ +output/ \ No newline at end of file diff --git a/packages/vue-to-dsl/README.md b/packages/vue-to-dsl/README.md new file mode 100644 index 0000000000..c041ca2597 --- /dev/null +++ b/packages/vue-to-dsl/README.md @@ -0,0 +1,224 @@ +# @opentiny/tiny-engine-vue-to-dsl + +> 将 Vue 代码文件/项目反向转换为 TinyEngine DSL Schema 的工具包 + +## 简介 + +`@opentiny/tiny-engine-vue-to-dsl` 解析 Vue 代码文件,生成可用于 TinyEngine 的 DSL Schema。同时内置“整包应用”转换能力,可从项目目录或 zip 包中聚合出 App 级 Schema(含 i18n、数据源、全局状态、页面元信息等)。 + +## 主要特性 + +- 支持模板、脚本(Options API / script setup)、样式的完整解析 +- 从 Vue 工程目录或 zip 文件生成 AppSchema +- 可配置组件映射、可插拔自定义解析器 +- TypeScript 实现,导出完整类型;提供单元与集成测试 + +## 安装 + +```bash +pnpm add @opentiny/tiny-engine-vue-to-dsl +``` + +## 目录结构 + +```text +src/ +├─ converter.ts # 主转换器(含 app 级聚合与 zip 支持) +├─ generator/ # schema 生成与归一 +├─ parser/ # SFC 粗分(template/script/style 块) +├─ parsers/ # 各块解析实现 +├─ constants.ts # 组件映射与组件包清单 +├─ index.ts # 包导出入口 +└─ types/ # 类型导出 +``` + +## 快速开始 + +```ts +import { VueToDslConverter } from '@opentiny/tiny-engine-vue-to-dsl'; + +const converter = new VueToDslConverter(); +const vueCode = ` + + + +`; + +const result = await converter.convertFromString(vueCode, 'Hello.vue'); +console.log(result.schema); +``` + +## API 概览 + +入口:`src/index.ts` + +导出: + +- `VueToDslConverter` 主转换器 +- 解析工具:`parseVueFile`、`parseSFC` +- 生成器:`generateSchema`、`generateAppSchema` +- 细分解析器:`parseTemplate`、`parseScript`、`parseStyle` +- 类型与常量:`types/*`、默认组件映射 `defaultComponentMap`、默认组件包清单 `defaultComponentsMap` + +### VueToDslConverter + +```ts +new VueToDslConverter(options?: VueToSchemaOptions) + +interface VueToSchemaOptions { + componentMap?: Record + preserveComments?: boolean + strictMode?: boolean + // 控制是否输出 computed 字段(默认 false) + computed_flag?: boolean + customParsers?: { + template?: { parse: (code: string) => any } + script?: { parse: (code: string) => any } + style?: { parse: (code: string) => any } + } + fileName?: string + path?: string + title?: string + description?: string +} + +type ConvertResult = { + schema: any | null + dependencies: string[] + errors: string[] + warnings: string[] +} +``` + +实例方法: + +- `convertFromString(code, fileName?)`:从字符串转换 +- `convertFromFile(filePath)`:从文件转换 +- `convertMultipleFiles(filePaths)`:批量转换 +- `convertAppDirectory(appDir)`:从工程目录(约定 src/ 结构)生成 App 级 schema +- `convertAppFromZip(zipBuffer)`:从 zip Buffer 生成 App 级 schema(Node 与浏览器均可用) +- `setOptions(partial)` / `getOptions()`:运行期更新/读取配置 + +### App 级聚合产物(convertAppDirectory/convertAppFromZip) + +输出结构(概要): + +```ts +{ + meta: { name, description, generatedAt, generator }, + i18n: { en_US: {}, zh_CN: {} }, + utils: Array<{ + name: string, + type: 'npm' | 'function', + content: { type: 'JSFunction', value: string, package?: string, destructuring?: boolean, exportName?: string } + }>, + dataSource: { list: any[] }, + globalState: Array<{ id: string, state: Record }>, + pageSchema: any[], + componentsMap: typeof defaultComponentsMap +} +``` + +数据来源约定: + +- 页面:`src/views/**/*.vue` +- i18n:`src/i18n/en_US.json`、`src/i18n/zh_CN.json` +- 工具函数:`src/utils.js`(简单 import/export 分析,支持命名/默认导入导出) +- 数据源:`src/lowcodeConfig/dataSource.json` +- 全局状态:`src/stores/*.js`(简易 Pinia `defineStore` 解析,只提取 state 返回对象) +- 路由:`src/router/index.js`(提取 name/path 与 import 的页面文件,设置 `meta.router/isPage/isHome`) + +## 模板/脚本/样式支持 + +模板(`parseTemplate`) + +- HTML 标签与自定义组件;通过 `componentMap` 做名称映射 +- 指令:`v-if`/`v-for`/`v-show`/`v-model`/`v-on`/`v-bind`/`v-slot` 等核心指令 +- v-for:尝试抽取迭代表达式,写入 `loop: { type: 'JSExpression', value: 'this.xxx' }` +- 事件与绑定:能解析简单字面量,复杂表达式以 `JSExpression` 形式保留 +- 文本与插值:转为 `Text` 组件;插值为 `JSExpression` +- 特殊:`tiny-icon-*` 归一为通用 `Icon` 组件并写入 `name` 属性 + +脚本(`parseScript`) + +- script setup: + - `reactive`/`ref` 识别到 state;`computed` 识别到 computed + - 顶层函数与返回对象内成员识别到 methods + - onMounted/onUpdated... 等生命周期识别 +- Options API: + - `props`(数组语法)/`methods`/`computed`/生命周期基础支持 +- import 收集:用于返回 `dependencies` + +样式(`parseStyle` + 辅助) + +- 基础样式串:直出 `css` +- 辅助能力:`parseCSSRules`、`extractCSSVariables`、`hasMediaQueries`、`extractMediaQueries` + +## 输出 Schema 约定(页面级) + +- 根节点 `componentName: 'Page'`,自动补齐 `id`(8 位字母数字) +- `state`/`methods`/`computed`/`lifecycle` 值以 `{ type: 'JSFunction', value: string }` 表达(state 中基础类型按需折叠) +- `children` 为模板树;属性中无法安全字面量化的表达式以 `JSExpression` 表达 +- 所有字符串做轻度“去换行/多空格”规整 + +## 测试用例说明 + +测试目录位于 `test/`,包含: + +- `test/sfc/`:单个 SFC 的基础转换测试 +- `test/testcases/`:按用例目录组织的场景测试(新增用例放这里) +- `test/full/`:整包项目/zip 的端到端转换测试 + +在本包目录 `packages/vue-to-dsl` 下使用 Vitest 进行单元与集成测试,运行: + +```bash +pnpm i +pnpm test +# 或 +npx vitest run +``` + +运行后会将每个用例的结果写入 `output/schema.json`,便于比对。 + +用例结构(示例): + +```text +test/testcases/ + 001_simple/ + input/component.vue # 输入 SFC + expected/schema.json # 期望 Schema(可为“子集”) + output/schema.json # 测试生成(自动写入) +``` + +断言规则(见 `test/testcases/index.test.js`): + +- 忽略动态字段:递归忽略所有层级的 `meta` 与 `id` +- 子集匹配:实际输出只需“包含” expected 的结构和值(数组按 expected 长度顺序比对前 N 项) +- 若 expected 含 `error: true`:仅断言发生错误并允许 schema 存在部分内容 + +因此 expected 可仅保留关键片段,无需完全复制整个 schema,适合 children 很多的页面。 + +新增用例步骤: + +1. 在 `test/testcases/` 新建目录(序号递增) +2. 添加 `input/component.vue` +3. 添加最小化 `expected/schema.json`(仅关键字段) +4. 运行测试,参考 `output/schema.json` 微调 expected + +组件映射: + +- 本测试文件内已设置常用 OpenTiny 组件映射(`tiny-form`、`tiny-grid`、`tiny-select`、`tiny-button-group`、`tiny-time-line` 等) +- 如使用未映射组件,可在测试中补充 `componentMap`,或在用例中用已映射组件替代 diff --git a/packages/vue-to-dsl/cli.ts b/packages/vue-to-dsl/cli.ts new file mode 100644 index 0000000000..3fabf3ea22 --- /dev/null +++ b/packages/vue-to-dsl/cli.ts @@ -0,0 +1,203 @@ +#!/usr/bin/env node + +/* eslint-disable no-console */ +/** + * Vue To DSL CLI Tool (TypeScript) + * 命令行工具,用于将Vue SFC文件转换为TinyEngine DSL Schema + */ + +import fs from 'fs/promises' +import path from 'path' +import { fileURLToPath } from 'url' +import { VueToDslConverter } from './src/converter' + +// 解决在 ESM 下使用 __dirname/__filename +const __filename = fileURLToPath(import.meta.url) + +// 命令行参数解析 +const args = process.argv.slice(2) + +const showHelp = args.includes('--help') || args.includes('-h') +if (args.length === 0 || showHelp) { + console.log(` +使用方法: + node ${path.basename(__filename)} [options] + +选项: + --output, -o 输出文件路径 + --format, -f 输出格式 (json, js) 默认: json + --help, -h 显示帮助信息 + --computed 启用输出 computed 字段(默认关闭) + +示例: + node ${path.basename(__filename)} ./components/MyComponent.vue + node ${path.basename(__filename)} ./components/MyComponent.vue --output ./output/schema.json + node ${path.basename(__filename)} ./components/MyComponent.vue --format js --output ./output/schema.js +`) + process.exit(0) +} + +// 解析参数 +const inputFile = args[0] +let outputFile: string | undefined +let format: 'json' | 'js' = 'json' +let computedFlag = false + +for (let i = 1; i < args.length; ) { + const option = args[i] + const value = args[i + 1] + + switch (option) { + case '--output': + case '-o': + outputFile = value + i += 2 + break + case '--format': + case '-f': + if (value === 'json' || value === 'js') { + format = value + } + i += 2 + break + case '--help': + case '-h': + console.log('显示帮助信息...') + process.exit(0) + break + case '--computed': + computedFlag = true + i += 1 + break + default: + // 跳过无法识别的参数,避免死循环 + i += 1 + } +} + +// 设置默认输出文件 +if (!outputFile) { + const baseName = path.basename(inputFile, '.vue') + outputFile = `${baseName}-schema.${format}` +} +const outputPath = outputFile as string + +/** + * 获取Schema统计信息 + */ +function getSchemaStats(schema: any) { + return { + stateCount: schema.state ? Object.keys(schema.state).length : 0, + methodCount: schema.methods ? Object.keys(schema.methods).length : 0, + computedCount: schema.computed ? Object.keys(schema.computed).length : 0, + lifecycleCount: schema.lifeCycles ? Object.keys(schema.lifeCycles).length : 0, + childrenCount: schema.children ? schema.children.length : 0, + cssLength: schema.css ? schema.css.length : 0 + } +} + +async function main() { + try { + console.log('🚀 开始转换Vue文件到DSL Schema...') + console.log(`📁 输入文件: ${inputFile}`) + console.log(`📄 输出文件: ${outputPath}`) + console.log(`📋 输出格式: ${format}`) + console.log() + + // 检查输入文件是否存在 + try { + await fs.access(inputFile) + } catch (error) { + console.error(`❌ 错误: 文件不存在 - ${inputFile}`) + process.exit(1) + } + + // 创建转换器 + const converter = new VueToDslConverter({ + componentMap: { + button: 'TinyButton', + input: 'TinyInput', + form: 'TinyForm' + }, + preserveComments: false, + strictMode: false, + computed_flag: computedFlag + }) + + // 执行转换 + const result = await converter.convertFromFile(inputFile) + + // 显示转换结果 + if (result.errors.length > 0) { + console.log('⚠️ 转换过程中的错误:') + result.errors.forEach((error: string) => console.log(` - ${error}`)) + console.log() + } + + if (result.warnings.length > 0) { + console.log('⚠️ 转换过程中的警告:') + result.warnings.forEach((warning: string) => console.log(` - ${warning}`)) + console.log() + } + + if (result.dependencies.length > 0) { + console.log('📦 发现的依赖项:') + result.dependencies.forEach((dep: string) => console.log(` - ${dep}`)) + console.log() + } + + if (!result.schema) { + console.error('❌ 转换失败,未生成Schema') + process.exit(1) + } + + // 生成输出内容 + let outputContent: string + if (format === 'json') { + outputContent = JSON.stringify(result.schema, null, 2) + } else if (format === 'js') { + outputContent = `// Generated DSL Schema from ${inputFile} +// Generated at: ${new Date().toISOString()} + +export default ${JSON.stringify(result.schema, null, 2)} +` + } else { + console.error(`❌ 错误: 不支持的输出格式 - ${format}`) + process.exit(1) + return + } + + // 确保输出目录存在 + const outputDir = path.dirname(outputPath) + if (outputDir !== '.' && outputDir !== '') { + await fs.mkdir(outputDir, { recursive: true }) + } + + // 写入输出文件 + await fs.writeFile(outputPath, outputContent, 'utf-8') + + console.log('✅ 转换完成!') + console.log(`📁 输出文件已保存到: ${outputPath}`) + + // 显示Schema统计信息 + const stats = getSchemaStats(result.schema) + console.log() + console.log('📊 Schema统计信息:') + console.log(` 组件名称: ${result.schema.componentName}`) + console.log(` 文件名称: ${result.schema.fileName}`) + console.log(` 状态数量: ${stats.stateCount}`) + console.log(` 方法数量: ${stats.methodCount}`) + console.log(` 计算属性: ${stats.computedCount}`) + console.log(` 生命周期: ${stats.lifecycleCount}`) + console.log(` 子组件数: ${stats.childrenCount}`) + console.log(` CSS长度: ${stats.cssLength} 字符`) + } catch (error: any) { + console.error('❌ 转换过程中发生错误:') + console.error(error?.message || error) + if (error?.stack) console.error(error.stack) + process.exit(1) + } +} + +// 运行主函数 +void main() diff --git a/packages/vue-to-dsl/package.json b/packages/vue-to-dsl/package.json new file mode 100644 index 0000000000..acbea172ed --- /dev/null +++ b/packages/vue-to-dsl/package.json @@ -0,0 +1,55 @@ +{ + "name": "@opentiny/tiny-engine-vue-to-dsl", + "version": "1.0.0", + "description": "Convert Vue SFC files back to TinyEngine DSL schema", + "publishConfig": { + "access": "public" + }, + "type": "module", + "main": "dist/tiny-engine-vue-to-dsl.cjs", + "module": "dist/tiny-engine-vue-to-dsl.js", + "types": "dist/index.d.ts", + "bin": { + "tiny-vue-to-dsl": "dist/cli.cjs" + }, + "files": [ + "dist" + ], + "scripts": { + "build": "pnpm run build:types && vite build && pnpm run build:cli", + "build:types": "tsc -p tsconfig.json", + "build:cli": "vite build --config vite.config.cli.mjs", + "test": "vitest", + "test:unit": "vitest run", + "coverage": "vitest run --coverage", + "dev": "vite build --watch" + }, + "repository": { + "type": "git", + "url": "https://github.com/opentiny/tiny-engine", + "directory": "packages/vue-to-dsl" + }, + "bugs": { + "url": "https://github.com/opentiny/tiny-engine/issues" + }, + "author": "OpenTiny Team", + "license": "MIT", + "homepage": "https://opentiny.design/tiny-engine", + "dependencies": { + "@babel/parser": "^7.23.0", + "@babel/traverse": "^7.23.0", + "@babel/types": "^7.23.0", + "@vue/compiler-dom": "^3.4.15", + "@vue/compiler-sfc": "^3.4.15", + "jszip": "^3.10.1", + "vue": "^3.4.15" + }, + "devDependencies": { + "@types/node": "^20.11.24", + "@vitest/coverage-v8": "^1.4.0", + "ts-node": "^10.9.2", + "typescript": "^5.4.0", + "vite": "^5.4.2", + "vitest": "^1.4.0" + } +} diff --git a/packages/vue-to-dsl/src/constants.ts b/packages/vue-to-dsl/src/constants.ts new file mode 100644 index 0000000000..2a77f66ce0 --- /dev/null +++ b/packages/vue-to-dsl/src/constants.ts @@ -0,0 +1,217 @@ +export const defaultComponentMap: Record = { + 'tiny-form': 'TinyForm', + 'tiny-form-item': 'TinyFormItem', + 'tiny-button': 'TinyButton', + 'tiny-button-group': 'TinyButtonGroup', + 'tiny-switch': 'TinySwitch', + 'tiny-select': 'TinySelect', + 'tiny-search': 'TinySearch', + 'tiny-input': 'TinyInput', + 'tiny-grid': 'TinyGrid', + 'tiny-grid-item': 'TinyGridItem', + 'tiny-col': 'TinyCol', + 'tiny-row': 'TinyRow', + 'tiny-time-line': 'TinyTimeLine', + 'tiny-card': 'TinyCard' +} + +export const defaultComponentsMap = [ + { + componentName: 'TinyCarouselItem', + package: '@opentiny/vue', + exportName: 'CarouselItem', + destructuring: true, + version: '3.24.0' + }, + { + componentName: 'TinyCheckboxButton', + package: '@opentiny/vue', + exportName: 'CheckboxButton', + destructuring: true, + version: '3.24.0' + }, + { componentName: 'TinyTree', package: '@opentiny/vue', exportName: 'Tree', destructuring: true, version: '3.24.0' }, + { + componentName: 'TinyPopover', + package: '@opentiny/vue', + exportName: 'Popover', + destructuring: true, + version: '3.24.0' + }, + { + componentName: 'TinyTooltip', + package: '@opentiny/vue', + exportName: 'Tooltip', + destructuring: true, + version: '3.2.0' + }, + { componentName: 'TinyCol', package: '@opentiny/vue', exportName: 'Col', destructuring: true, version: '3.24.0' }, + { + componentName: 'TinyDropdownItem', + package: '@opentiny/vue', + exportName: 'DropdownItem', + destructuring: true, + version: '3.24.0' + }, + { componentName: 'TinyPager', package: '@opentiny/vue', exportName: 'Pager', destructuring: true, version: '3.24.0' }, + { + componentName: 'TinyPlusAccessdeclined', + package: '@opentiny/vue', + exportName: 'AccessDeclined', + destructuring: true, + version: '3.4.1' + }, + { + componentName: 'TinyPlusFrozenPage', + package: '@opentiny/vue', + exportName: 'FrozenPage', + destructuring: true, + version: '3.4.1' + }, + { + componentName: 'TinyPlusNonSupportRegion', + package: '@opentiny/vue', + exportName: 'NonSupportRegion', + destructuring: true, + version: '3.4.1' + }, + { + componentName: 'TinyPlusBeta', + package: '@opentiny/vue', + exportName: 'Beta', + destructuring: true, + version: '3.4.1' + }, + { + componentName: 'TinySearch', + package: '@opentiny/vue', + exportName: 'Search', + destructuring: true, + version: '3.24.0' + }, + { componentName: 'TinyRow', package: '@opentiny/vue', exportName: 'Row', destructuring: true, version: '3.24.0' }, + { + componentName: 'TinyFormItem', + package: '@opentiny/vue', + exportName: 'FormItem', + destructuring: true, + version: '3.24.0' + }, + { componentName: 'TinyAlert', package: '@opentiny/vue', exportName: 'Alert', destructuring: true, version: '3.2.0' }, + { componentName: 'TinyInput', package: '@opentiny/vue', exportName: 'Input', destructuring: true, version: '3.24.0' }, + { componentName: 'TinyTabs', package: '@opentiny/vue', exportName: 'Tabs', destructuring: true, version: '3.24.0' }, + { + componentName: 'TinyDropdownMenu', + package: '@opentiny/vue', + exportName: 'DropdownMenu', + destructuring: true, + version: '3.24.0' + }, + { + componentName: 'TinyDialogBox', + package: '@opentiny/vue', + exportName: 'DialogBox', + destructuring: true, + version: '3.2.0' + }, + { + componentName: 'TinySwitch', + package: '@opentiny/vue', + exportName: 'Switch', + destructuring: true, + version: '3.24.0' + }, + { + componentName: 'TinyTimeLine', + package: '@opentiny/vue', + exportName: 'TimeLine', + destructuring: true, + version: '3.24.0' + }, + { + componentName: 'TinyTabItem', + package: '@opentiny/vue', + exportName: 'TabItem', + destructuring: true, + version: '3.24.0' + }, + { componentName: 'TinyRadio', package: '@opentiny/vue', exportName: 'Radio', destructuring: true, version: '3.24.0' }, + { componentName: 'TinyForm', package: '@opentiny/vue', exportName: 'Form', destructuring: true, version: '3.24.0' }, + { componentName: 'TinyGrid', package: '@opentiny/vue', exportName: 'Grid', destructuring: true, version: '3.24.0' }, + { + componentName: 'TinyNumeric', + package: '@opentiny/vue', + exportName: 'Numeric', + destructuring: true, + version: '3.24.0' + }, + { + componentName: 'TinyCheckboxGroup', + package: '@opentiny/vue', + exportName: 'CheckboxGroup', + destructuring: true, + version: '3.24.0' + }, + { + componentName: 'TinySelect', + package: '@opentiny/vue', + exportName: 'Select', + destructuring: true, + version: '3.24.0' + }, + { + componentName: 'TinyButtonGroup', + package: '@opentiny/vue', + exportName: 'ButtonGroup', + destructuring: true, + version: '3.24.0' + }, + { + componentName: 'TinyButton', + package: '@opentiny/vue', + exportName: 'Button', + destructuring: true, + version: '3.24.0' + }, + { + componentName: 'TinyCarousel', + package: '@opentiny/vue', + exportName: 'Carousel', + destructuring: true, + version: '3.24.0' + }, + { + componentName: 'TinyPopeditor', + package: '@opentiny/vue', + exportName: 'Popeditor', + destructuring: true, + version: '3.24.0' + }, + { + componentName: 'TinyDatePicker', + package: '@opentiny/vue', + exportName: 'DatePicker', + destructuring: true, + version: '3.24.0' + }, + { + componentName: 'TinyDropdown', + package: '@opentiny/vue', + exportName: 'Dropdown', + destructuring: true, + version: '0.1.20' + }, + { + componentName: 'TinyChartHistogram', + package: '@opentiny/vue', + exportName: 'ChartHistogram', + destructuring: true, + version: '3.24.0' + }, + { componentName: 'PortalHome', main: 'common/components/home', destructuring: false, version: '1.0.0' }, + { componentName: 'PreviewBlock1', main: 'preview', destructuring: false, version: '1.0.0' }, + { componentName: 'PortalHeader', main: 'common', destructuring: false, version: '1.0.0' }, + { componentName: 'PortalBlock', main: 'portal', destructuring: false, version: '1.0.0' }, + { componentName: 'PortalPermissionBlock', main: '', destructuring: false, version: '1.0.0' }, + { componentName: 'TinyCard', exportName: 'Card', package: '@opentiny/vue', version: '^3.10.0', destructuring: true } +] diff --git a/packages/vue-to-dsl/src/converter.ts b/packages/vue-to-dsl/src/converter.ts new file mode 100644 index 0000000000..67f653948f --- /dev/null +++ b/packages/vue-to-dsl/src/converter.ts @@ -0,0 +1,825 @@ +import { parseSFC } from './parser/index' +import { parseTemplate } from './parsers/templateParser' +import { parseScript } from './parsers/scriptParser' +import { parseStyle } from './parsers/styleParser' +import { generateSchema, generateAppSchema } from './generator/index' +import { defaultComponentMap } from './constants' +import fs from 'fs/promises' +import path from 'path' +import os from 'os' +import JSZip from 'jszip' + +export interface VueToSchemaOptions { + componentMap?: Record + preserveComments?: boolean + strictMode?: boolean + // 控制是否在出码结果中包含 computed 字段,默认 false + computed_flag?: boolean + customParsers?: { + template?: { parse: (code: string) => any } + script?: { parse: (code: string) => any } + style?: { parse: (code: string) => any } + } + fileName?: string + path?: string + title?: string + description?: string +} + +export interface ConvertResult { + schema: any | null + dependencies: string[] + errors: string[] + warnings: string[] +} + +export class VueToDslConverter { + private options: VueToSchemaOptions + + constructor(options: VueToSchemaOptions = {}) { + this.options = { + componentMap: defaultComponentMap, + preserveComments: false, + strictMode: false, + computed_flag: false, + customParsers: {}, + ...options + } + } + + async convertFromString(vueCode: string, fileName?: string): Promise { + const errors: string[] = [] + const warnings: string[] = [] + const dependencies: string[] = [] + + try { + const sfcResult = parseSFC(vueCode) + if (!sfcResult.template && !sfcResult.scriptSetup && !sfcResult.script) { + throw new Error('Invalid Vue SFC: no template or script found') + } + + let templateSchema: any[] = [] + let scriptSchema: any = {} + let styleSchema: any = {} + + if (sfcResult.template) { + try { + templateSchema = this.options.customParsers?.template + ? this.options.customParsers.template.parse(sfcResult.template) + : parseTemplate(sfcResult.template, this.options as any) + } catch (error: any) { + errors.push(`Template parsing error: ${error.message}`) + if (this.options.strictMode) throw error + } + } + + const scriptContent = sfcResult.scriptSetup || sfcResult.script + if (scriptContent) { + try { + scriptSchema = this.options.customParsers?.script + ? this.options.customParsers.script.parse(scriptContent) + : parseScript(scriptContent, { + isSetup: !!sfcResult.scriptSetup, + ...(this.options as any) + }) + + if (scriptSchema.imports) { + dependencies.push(...scriptSchema.imports.map((imp: any) => imp.source)) + } + + // Surface script parser soft errors returned by parseScript + if ((scriptSchema as any).error) { + const msg = (scriptSchema as any).error + errors.push(`Script parsing error: ${msg}`) + if (this.options.strictMode) throw new Error(msg) + } + } catch (error: any) { + errors.push(`Script parsing error: ${error.message}`) + if (this.options.strictMode) throw error + } + } + + if (sfcResult.style) { + try { + styleSchema = this.options.customParsers?.style + ? this.options.customParsers.style.parse(sfcResult.style) + : parseStyle(sfcResult.style, this.options as any) + } catch (error: any) { + errors.push(`Style parsing error: ${error.message}`) + if (this.options.strictMode) throw error + } + } + + // Set fileName in options for schema generation + if (fileName) { + this.options.fileName = fileName.replace(/\.vue$/i, '') + } + + const schema = await generateSchema(templateSchema, scriptSchema, styleSchema, this.options as any) + + return { + schema, + dependencies: [...new Set(dependencies)], + errors, + warnings + } + } catch (error: any) { + errors.push(`Conversion error: ${error.message}`) + return { schema: null, dependencies: [], errors, warnings } + } + } + + async convertFromFile(filePath: string): Promise { + try { + const vueCode = await fs.readFile(filePath, 'utf-8') + const fileName = path.basename(filePath, '.vue') + const result = await this.convertFromString(vueCode, fileName) + return result + } catch (error: any) { + return { schema: null, dependencies: [], errors: [`File reading error: ${error.message}`], warnings: [] } + } + } + + async convertMultipleFiles(filePaths: string[]): Promise { + const results: ConvertResult[] = [] + for (const filePath of filePaths) { + try { + const result = await this.convertFromFile(filePath) + results.push(result) + } catch (error: any) { + results.push({ + schema: null, + dependencies: [], + errors: [`Failed to convert ${filePath}: ${error.message}`], + warnings: [] + }) + } + } + return results + } + + // Recursively walk a directory and collect files that match a predicate + private async walk(dir: string, filter: (p: string, stat: any) => boolean, acc: string[] = []): Promise { + try { + const entries = await fs.readdir(dir, { withFileTypes: true } as any) + for (const entry of entries as any[]) { + const p = path.join(dir, entry.name) + if (entry.isDirectory()) { + await this.walk(p, filter, acc) + } else if (entry.isFile() && filter(p, entry)) { + acc.push(p) + } + } + } catch { + // ignore missing dirs + } + return acc + } + + // Convert a full app directory (e.g., test/full/input/appdemo01) into an aggregated schema.json + async convertAppDirectory(appDir: string): Promise { + const srcDir = path.join(appDir, 'src') + const viewsDir = path.join(srcDir, 'views') + + // 1) Collect page schemas from all .vue files under src/views/** + const vueFiles = await this.walk(viewsDir, (p) => p.endsWith('.vue')) + const pageResults = await this.convertMultipleFiles(vueFiles) + const pageSchemas = pageResults.map((r) => r.schema).filter(Boolean) + + // 2) Load i18n + let i18n: any = { en_US: {}, zh_CN: {} } + try { + const enPath = path.join(srcDir, 'i18n', 'en_US.json') + const zhPath = path.join(srcDir, 'i18n', 'zh_CN.json') + const [en, zh] = await Promise.all([ + fs.readFile(enPath, 'utf-8').catch(() => '{}'), + fs.readFile(zhPath, 'utf-8').catch(() => '{}') + ]) + i18n = { en_US: JSON.parse(en), zh_CN: JSON.parse(zh) } + } catch { + // keep defaults + } + + // 3) Load utils from src/utils.js (very lightweight parser) + const utils: any[] = [] + try { + const utilsPath = path.join(srcDir, 'utils.js') + const code = await fs.readFile(utilsPath, 'utf-8') + const importRegex = /import\s+(?:{\s*([\w,\s]+)\s*}|([\w$]+))\s+from\s+['"]([^'"]+)['"]/g + const imports: Array<{ local: string; source: string; destructuring: boolean }> = [] + let m: RegExpExecArray | null + while ((m = importRegex.exec(code))) { + const named = m[1] + const def = m[2] + const source = m[3] + if (named) { + named + .split(',') + .map((s) => s.trim()) + .filter(Boolean) + .forEach((n) => imports.push({ local: n, source, destructuring: true })) + } else if (def) { + imports.push({ local: def, source, destructuring: false }) + } + } + // exported names + const exportRegex = /export\s*{([^}]+)}/ + const expMatch = code.match(exportRegex) + const exported = expMatch + ? expMatch[1] + .split(',') + .map((s) => s.trim()) + .filter(Boolean) + : [] + // Build utils array for exported imports; fallback for exported local functions/vars + for (const name of exported) { + const found = imports.find((imp) => imp.local === name) + if (found) { + utils.push({ + name, + type: 'npm', + content: { + type: 'JSFunction', + value: '', + package: found.source, + destructuring: found.destructuring, + exportName: name + } + }) + } else { + // treat as function utility placeholder + utils.push({ name, type: 'function', content: { type: 'JSFunction', value: '' } }) + } + } + } catch { + // ignore + } + + // 4) Load dataSource from lowcodeConfig/dataSource.json + const dataSource: any = { list: [] } + try { + const dsPath = path.join(srcDir, 'lowcodeConfig', 'dataSource.json') + const dsRaw = await fs.readFile(dsPath, 'utf-8') + const dsJson = JSON.parse(dsRaw) + // pass through; keep shape as-is + if (Array.isArray(dsJson.list)) dataSource.list = dsJson.list + } catch { + // ignore + } + + // 5) Load globalState from src/stores/*.js (very light support for pinia defineStore) + const globalState: any[] = [] + try { + const storesDir = path.join(srcDir, 'stores') + const storeFiles = await this.walk(storesDir, (p) => p.endsWith('.js')) + for (const sf of storeFiles) { + const code = await fs.readFile(sf, 'utf-8') + // Skip files that don't define a Pinia store (e.g., re-export index.js) + if (!/defineStore\s*\(/.test(code)) continue + // naive extraction: id: 'xxx' + const idMatch = code.match(/id:\s*['"]([^'"]+)['"]/) + const stateMatch = code.match(/state:\s*\(\)\s*=>\s*\((\{[\s\S]*?\})\)/) + const entry: any = { id: idMatch ? idMatch[1] : path.basename(sf, path.extname(sf)) } + if (stateMatch) { + try { + // very naive: turn JS object to JSON by removing trailing commas and function values + const objText = stateMatch[1] + const stateObj = Function(`return (${objText})`)() + entry.state = stateObj + } catch { + entry.state = {} + } + } else { + // No state found, skip this file to avoid empty entries + continue + } + // Only push when we have some keys in state (avoid empty {}) + if (entry.state && typeof entry.state === 'object' && Object.keys(entry.state).length > 0) { + globalState.push(entry) + } + } + } catch { + // ignore + } + + // 6) Read router info to enrich page meta (router path, isPage, isHome) + try { + const routerPath = path.join(srcDir, 'router', 'index.js') + const rcode = await fs.readFile(routerPath, 'utf-8') + // find root redirect name (home) + // Simply capture the first redirect name (root level in this project) + const homeMatch = rcode.match(/redirect:\s*\{\s*name:\s*['"]([^'"]+)['"]/) + const homeName = homeMatch ? homeMatch[1] : '' + + // To avoid incorrectly pairing the redirect name with the first route's path/component, + // remove the redirect object before extracting route entries. + const rclean = rcode.replace(/redirect\s*:\s*\{[\s\S]*?\}/, '') + + const routeEntries: Array<{ routeName: string; routePath: string; importPath: string }> = [] + const routeRegex = + /name:\s*['"]([^'"]+)['"][\s\S]*?path:\s*['"]([^'"]+)['"][\s\S]*?component:\s*\(\)\s*=>\s*import\(\s*['"]([^'"]+)['"]\s*\)/g + let m: RegExpExecArray | null + while ((m = routeRegex.exec(rclean))) { + routeEntries.push({ routeName: m[1], routePath: m[2], importPath: m[3] }) + } + // Build map by fileName (basename of the import .vue) + const byFile: Record = {} + for (const e of routeEntries) { + const base = path.basename(e.importPath).replace(/\.vue$/i, '') + byFile[base] = { routeName: e.routeName, routePath: e.routePath, isHome: e.routeName === homeName } + } + // Enrich page schemas + for (const ps of pageSchemas) { + const fileName = ps?.fileName + if (!fileName) continue + const info = byFile[fileName] + if (!info) continue + ps.meta = ps.meta || {} + ps.meta.router = info.routePath.startsWith('/') ? info.routePath.slice(1) : info.routePath + ps.meta.isPage = true + ps.meta.isHome = !!info.isHome + } + } catch { + // ignore router enrichment failures + } + + // 7) Assemble app schema + const appSchema = generateAppSchema(pageSchemas, { + i18n, + utils, + dataSource, + globalState + }) + + return appSchema + } + + setOptions(options: VueToSchemaOptions) { + this.options = { ...this.options, ...options } + } + + getOptions(): VueToSchemaOptions { + return { ...this.options } + } + + // Convert an app from a zip buffer (in-memory). The buffer should be the content of the zip file (not a path). + async convertAppFromZip(zipBuffer: ArrayBuffer | Uint8Array | Buffer): Promise { + // Browser-safe path: avoid fs/path/os, work fully in-memory + if (typeof window !== 'undefined' && typeof (window as any).document !== 'undefined') { + const zip = await JSZip.loadAsync(zipBuffer as any) + + // Collect file entries (posix paths in zip) + const allFiles = Object.keys((zip as any).files || {}) + .filter((p) => !(zip as any).files[p].dir) + .filter((p) => !p.startsWith('__MACOSX/')) + + // Determine root prefix (top-level folder) + const topLevels = new Set( + allFiles.map((p) => p.split('/')[0]).filter((seg) => !!seg && seg !== '.' && seg !== '..') + ) + let rootPrefix = '' + if (topLevels.size === 1) rootPrefix = [...topLevels][0] + '/' + + const joinRoot = (sub: string) => (rootPrefix ? rootPrefix + sub.replace(/^\/+/, '') : sub.replace(/^\/+/, '')) + const readText = async (rel: string) => { + const file = zip.file(rel) + return file ? await file.async('string') : null + } + + // 1) Pages: src/views/**/*.vue + const viewPrefix = joinRoot('src/views/') + const vueFiles = allFiles.filter((p) => p.startsWith(viewPrefix) && p.endsWith('.vue')) + const pageSchemas: any[] = [] + for (const vf of vueFiles) { + const code = await readText(vf) + if (!code) continue + const base = vf.split('/').pop() || 'Page.vue' + const fileName = base.replace(/\.vue$/i, '') + const res = await this.convertFromString(code, fileName) + if (res.schema) pageSchemas.push(res.schema) + } + + // 2) i18n + let i18n: any = { en_US: {}, zh_CN: {} } + try { + const en = (await readText(joinRoot('src/i18n/en_US.json'))) || '{}' + const zh = (await readText(joinRoot('src/i18n/zh_CN.json'))) || '{}' + i18n = { en_US: JSON.parse(en), zh_CN: JSON.parse(zh) } + } catch { + // keep defaults + } + + // 3) utils from src/utils.js + const utils: any[] = [] + try { + const code = await readText(joinRoot('src/utils.js')) + if (code) { + const importRegex = /import\s+(?:{\s*([\w,\s]+)\s*}|([\w$]+))\s+from\s+['"]([^'"]+)['"]/g + const imports: Array<{ local: string; source: string; destructuring: boolean }> = [] + let m: RegExpExecArray | null + while ((m = importRegex.exec(code))) { + const named = m[1] + const def = m[2] + const source = m[3] + if (named) { + named + .split(',') + .map((s) => s.trim()) + .filter(Boolean) + .forEach((n) => imports.push({ local: n, source, destructuring: true })) + } else if (def) { + imports.push({ local: def, source, destructuring: false }) + } + } + const exportRegex = /export\s*{([^}]+)}/ + const expMatch = code.match(exportRegex) + const exported = expMatch + ? expMatch[1] + .split(',') + .map((s) => s.trim()) + .filter(Boolean) + : [] + for (const name of exported) { + const found = imports.find((imp) => imp.local === name) + if (found) { + utils.push({ + name, + type: 'npm', + content: { + type: 'JSFunction', + value: '', + package: found.source, + destructuring: found.destructuring, + exportName: name + } + }) + } else { + utils.push({ name, type: 'function', content: { type: 'JSFunction', value: '' } }) + } + } + } + } catch { + // ignore + } + + // 4) dataSource + const dataSource: any = { list: [] } + try { + const dsRaw = await readText(joinRoot('src/lowcodeConfig/dataSource.json')) + if (dsRaw) { + const dsJson = JSON.parse(dsRaw) + if (Array.isArray(dsJson.list)) dataSource.list = dsJson.list + } + } catch { + // ignore + } + + // 5) globalState from src/stores/*.js + const storesPrefix = joinRoot('src/stores/') + const storeFiles = allFiles.filter((p) => p.startsWith(storesPrefix) && p.endsWith('.js')) + const globalState: any[] = [] + for (const sf of storeFiles) { + try { + const code = await readText(sf) + if (!code || !/defineStore\s*\(/.test(code)) continue + const idMatch = code.match(/id:\s*['"]([^'"]+)['"]/) + const stateMatch = code.match(/state:\s*\(\)\s*=>\s*\((\{[\s\S]*?\})\)/) + const entry: any = { id: idMatch ? idMatch[1] : (sf.split('/').pop() || 'store').replace(/\.[^.]+$/, '') } + if (stateMatch) { + try { + const objText = stateMatch[1] + const stateObj = Function(`return (${objText})`)() + entry.state = stateObj + } catch { + entry.state = {} + } + } else { + continue + } + if (entry.state && typeof entry.state === 'object' && Object.keys(entry.state).length > 0) { + globalState.push(entry) + } + } catch { + // ignore + } + } + + // 6) router enrichment + try { + const rcode = await readText(joinRoot('src/router/index.js')) + if (rcode) { + const homeMatch = rcode.match(/redirect:\s*\{\s*name:\s*['"]([^'"]+)['"]/) + const homeName = homeMatch ? homeMatch[1] : '' + const rclean = rcode.replace(/redirect\s*:\s*\{[\s\S]*?\}/, '') + const routeEntries: Array<{ routeName: string; routePath: string; importPath: string }> = [] + const routeRegex = + /name:\s*['"]([^'"]+)['"][\s\S]*?path:\s*['"]([^'"]+)['"][\s\S]*?component:\s*\(\)\s*=>\s*import\(\s*['"]([^'"]+)['"]\s*\)/g + let m: RegExpExecArray | null + while ((m = routeRegex.exec(rclean))) + routeEntries.push({ routeName: m[1], routePath: m[2], importPath: m[3] }) + const byFile: Record = {} + for (const e of routeEntries) { + const base = (e.importPath.split('/').pop() || '').replace(/\.vue$/i, '') + byFile[base] = { routeName: e.routeName, routePath: e.routePath, isHome: e.routeName === homeName } + } + for (const ps of pageSchemas) { + const fileName = ps?.fileName + if (!fileName) continue + const info = byFile[fileName] + if (!info) continue + ps.meta = ps.meta || {} + ps.meta.router = info.routePath.startsWith('/') ? info.routePath.slice(1) : info.routePath + ps.meta.isPage = true + ps.meta.isHome = !!info.isHome + } + } + } catch { + // ignore + } + + // 7) Assemble app schema + const appSchema = generateAppSchema(pageSchemas, { + i18n, + utils, + dataSource, + globalState + }) + + return appSchema + } + + // Node.js path: unzip to temp and reuse directory-based converter + // 1) Unzip into a temp directory + const tmpBase = await fs.mkdtemp(path.join(os.tmpdir(), 'vue-to-dsl-')) + const zip = await JSZip.loadAsync(zipBuffer as any) + + const fileEntries: string[] = [] + const writeTasks: Promise[] = [] + zip.forEach((relPath, file) => { + // Skip macOS metadata + if (relPath.startsWith('__MACOSX/')) return + const outPath = path.join(tmpBase, relPath) + if (file.dir) { + writeTasks.push(fs.mkdir(outPath, { recursive: true })) + } else { + fileEntries.push(relPath) + writeTasks.push( + (async () => { + await fs.mkdir(path.dirname(outPath), { recursive: true }) + const content = await file.async('nodebuffer') + await fs.writeFile(outPath, content) + })() + ) + } + }) + await Promise.all(writeTasks) + + // 2) Determine the root app directory inside the zip + const topLevels = new Set( + fileEntries.map((p) => p.split('/')[0]).filter((seg) => !!seg && seg !== '.' && seg !== '..') + ) + + let appRoot = tmpBase + if (topLevels.size === 1) { + const only = [...topLevels][0] + appRoot = path.join(tmpBase, only) + } + + // 3) Delegate to convertAppDirectory + const schema = await this.convertAppDirectory(appRoot) + return schema + } + + async convertAppFromDirectory(files: FileList): Promise { + const fileArray = Array.from(files) + let relevantFiles = [] + + const readText = async (file: File) => { + return new Promise((resolve, reject) => { + const reader = new FileReader() + reader.onload = () => resolve(reader.result as string) + reader.onerror = () => reject(reader.error) + reader.readAsText(file) + }) + } + + const createGitignoreFilter = (gitignoreContent: string) => { + const lines = gitignoreContent + .split('\n') + .map((l) => l.trim()) + .filter((l) => l && !l.startsWith('#')) + const patterns = lines.map((line) => { + const isNegative = line.startsWith('!') + const pattern = isNegative ? line.slice(1) : line + + // Convert gitignore pattern to regex + const regexString = pattern + .replace(/([.+?^${}()|[\]\\])/g, '\\$1') // Escape special chars + .replace(/\/\*\*$/, '/.*') // '/**' at the end + .replace(/\*\*/g, '.*') // '**' + .replace(/\*/g, '[^/]*') // '*' + .replace(/\?/g, '[^/]') // '?' + + // Handle directory matching + if (regexString.endsWith('/')) { + return { regex: new RegExp(`^${regexString}`), isNegative } + } + + return { regex: new RegExp(`^${regexString}(/.*)?$`), isNegative } + }) + + return (path: string) => { + let isIgnored = false + for (const { regex, isNegative } of patterns) { + if (regex.test(path)) { + isIgnored = !isNegative + } + } + return !isIgnored + } + } + + const gitignoreFile = fileArray.find((file) => file.webkitRelativePath.endsWith('/.gitignore')) + + if (gitignoreFile) { + const gitignoreContent = await readText(gitignoreFile) + const rootDir = gitignoreFile.webkitRelativePath.split('/')[0] + const filter = createGitignoreFilter(gitignoreContent) + + relevantFiles = fileArray.filter((file) => { + const relativePath = file.webkitRelativePath.slice(rootDir.length + 1) + return relativePath && filter(relativePath) + }) + } else { + // Filter out node_modules + relevantFiles = fileArray.filter((file) => !file.webkitRelativePath.includes('node_modules')) + } + + // 1) Pages: src/views/**/*.vue + const vueFiles = relevantFiles.filter( + (file) => file.webkitRelativePath.includes('src/views/') && file.name.endsWith('.vue') + ) + const pageSchemas: any[] = [] + for (const vf of vueFiles) { + const code = await readText(vf) + if (!code) continue + const base = vf.name || 'Page.vue' + const fileName = base.replace(/\.vue$/i, '') + const res = await this.convertFromString(code, fileName) + if (res.schema) pageSchemas.push(res.schema) + } + + // 2) i18n + let i18n: any = { en_US: {}, zh_CN: {} } + try { + const enFile = relevantFiles.find((f) => f.webkitRelativePath.endsWith('src/i18n/en_US.json')) + const zhFile = relevantFiles.find((f) => f.webkitRelativePath.endsWith('src/i18n/zh_CN.json')) + const en = enFile ? await readText(enFile) : '{}' + const zh = zhFile ? await readText(zhFile) : '{}' + i18n = { en_US: JSON.parse(en), zh_CN: JSON.parse(zh) } + } catch { + // keep defaults + } + + // 3) utils from src/utils.js + const utils: any[] = [] + try { + const utilsFile = relevantFiles.find((f) => f.webkitRelativePath.endsWith('src/utils.js')) + if (utilsFile) { + const code = await readText(utilsFile) + const importRegex = /import\s+(?:{\s*([\w,\s]+)\s*}|([\w$]+))\s+from\s+['"]([^'"]+)['"]/g + const imports: Array<{ local: string; source: string; destructuring: boolean }> = [] + let m: RegExpExecArray | null + while ((m = importRegex.exec(code))) { + const named = m[1] + const def = m[2] + const source = m[3] + if (named) { + named + .split(',') + .map((s) => s.trim()) + .filter(Boolean) + .forEach((n) => imports.push({ local: n, source, destructuring: true })) + } else if (def) { + imports.push({ local: def, source, destructuring: false }) + } + } + const exportRegex = /export\s*{([^}]+)}/ + const expMatch = code.match(exportRegex) + const exported = expMatch + ? expMatch[1] + .split(',') + .map((s) => s.trim()) + .filter(Boolean) + : [] + for (const name of exported) { + const found = imports.find((imp) => imp.local === name) + if (found) { + utils.push({ + name, + type: 'npm', + content: { + type: 'JSFunction', + value: '', + package: found.source, + destructuring: found.destructuring, + exportName: name + } + }) + } else { + utils.push({ name, type: 'function', content: { type: 'JSFunction', value: '' } }) + } + } + } + } catch { + // ignore + } + + // 4) dataSource + const dataSource: any = { list: [] } + try { + const dsFile = relevantFiles.find((f) => f.webkitRelativePath.endsWith('src/lowcodeConfig/dataSource.json')) + if (dsFile) { + const dsRaw = await readText(dsFile) + const dsJson = JSON.parse(dsRaw) + if (Array.isArray(dsJson.list)) dataSource.list = dsJson.list + } + } catch { + // ignore + } + + // 5) globalState from src/stores/*.js + const storeFiles = relevantFiles.filter( + (f) => f.webkitRelativePath.includes('src/stores/') && f.name.endsWith('.js') + ) + const globalState: any[] = [] + for (const sf of storeFiles) { + try { + const code = await readText(sf) + if (!code || !/defineStore\s*\(/.test(code)) continue + const idMatch = code.match(/id:\s*['"]([^'"]+)['"]/) + const stateMatch = code.match(/state:\s*\(\)\s*=>\s*\((\{[\s\S]*?\})\)/) + const entry: any = { id: idMatch ? idMatch[1] : sf.name.replace(/\.[^.]+$/, '') } + if (stateMatch) { + try { + const objText = stateMatch[1] + const stateObj = Function(`return (${objText})`)() + entry.state = stateObj + } catch { + entry.state = {} + } + } else { + continue + } + if (entry.state && typeof entry.state === 'object' && Object.keys(entry.state).length > 0) { + globalState.push(entry) + } + } catch { + // ignore + } + } + + // 6) router enrichment + try { + const routerFile = relevantFiles.find((f) => f.webkitRelativePath.endsWith('src/router/index.js')) + if (routerFile) { + const rcode = await readText(routerFile) + const homeMatch = rcode.match(/redirect:\s*\{\s*name:\s*['"]([^'"]+)['"]/) + const homeName = homeMatch ? homeMatch[1] : '' + const rclean = rcode.replace(/redirect\s*:\s*\{[\s\S]*?\}/, '') + const routeEntries: Array<{ routeName: string; routePath: string; importPath: string }> = [] + const routeRegex = + /name:\s*['"]([^'"]+)['"][\s\S]*?path:\s*['"]([^'"]+)['"][\s\S]*?component:\s*\(\)\s*=>\s*import\(\s*['"]([^'"]+)['"]\s*\)/g + let m: RegExpExecArray | null + while ((m = routeRegex.exec(rclean))) routeEntries.push({ routeName: m[1], routePath: m[2], importPath: m[3] }) + const byFile: Record = {} + for (const e of routeEntries) { + const base = (e.importPath.split('/').pop() || '').replace(/\.vue$/i, '') + byFile[base] = { routeName: e.routeName, routePath: e.routePath, isHome: e.routeName === homeName } + } + for (const ps of pageSchemas) { + const fileName = ps?.fileName + if (!fileName) continue + const info = byFile[fileName] + if (!info) continue + ps.meta = ps.meta || {} + ps.meta.router = info.routePath.startsWith('/') ? info.routePath.slice(1) : info.routePath + ps.meta.isPage = true + ps.meta.isHome = !!info.isHome + } + } + } catch { + // ignore + } + + // 7) Assemble app schema + const appSchema = generateAppSchema(pageSchemas, { + i18n, + utils, + dataSource, + globalState + }) + + return appSchema + } +} diff --git a/packages/vue-to-dsl/src/generator/index.ts b/packages/vue-to-dsl/src/generator/index.ts new file mode 100644 index 0000000000..dba02dfe23 --- /dev/null +++ b/packages/vue-to-dsl/src/generator/index.ts @@ -0,0 +1,173 @@ +import { defaultComponentsMap } from '../constants' +function convertToPlainValue(expr: any) { + if (typeof expr !== 'string') return expr + const trimmed = expr.trim() + if (/^['"].*['"]$/.test(trimmed)) return trimmed.slice(1, -1) + if (/^-?\d+(\.\d+)?$/.test(trimmed)) return Number(trimmed) + if (trimmed === 'true') return true + if (trimmed === 'false') return false + if (trimmed === 'null') return null + return trimmed +} + +function extractRefPrimitive(expr: any) { + if (typeof expr !== 'string') return expr + const m = expr.match(/^ref\((.*)\)$/) + if (!m) return expr + const inner = m[1].trim() + return convertToPlainValue(inner) +} + +function transformState(state: Record) { + const result: Record = {} + Object.keys(state).forEach((key) => { + const stateItem = state[key] + if (typeof stateItem === 'object' && stateItem.type) { + switch (stateItem.type) { + case 'reactive': + result[key] = convertToPlainValue(stateItem.value) + break + case 'ref': + result[key] = extractRefPrimitive(stateItem.value) + break + default: + result[key] = stateItem.value || stateItem + } + } else { + result[key] = stateItem + } + }) + return result +} + +function transformMethods(methods: Record) { + const result: Record = {} + Object.keys(methods).forEach((key) => { + const method = methods[key] + if (typeof method === 'object' && method.value) { + result[key] = { type: 'JSFunction', value: method.value } + } else if (typeof method === 'string') { + result[key] = { type: 'JSFunction', value: method } + } else { + result[key] = { type: 'JSFunction', value: 'function() { /* method implementation */ }' } + } + }) + return result +} + +function transformComputed(computed: Record) { + const result: Record = {} + Object.keys(computed).forEach((key) => { + const computedItem = computed[key] + if (typeof computedItem === 'object' && computedItem.value) { + result[key] = { type: 'JSFunction', value: computedItem.value } + } else if (typeof computedItem === 'string') { + result[key] = { type: 'JSFunction', value: computedItem } + } else { + result[key] = { type: 'JSFunction', value: 'function() { /* computed getter */ }' } + } + }) + return result +} + +function transformLifeCycles(lifecycle: Record) { + const result: Record = {} + Object.keys(lifecycle).forEach((key) => { + const lifecycleItem = lifecycle[key] + if (typeof lifecycleItem === 'object' && lifecycleItem.value) { + result[key] = { type: 'JSFunction', value: lifecycleItem.value } + } else if (typeof lifecycleItem === 'string') { + result[key] = { type: 'JSFunction', value: lifecycleItem } + } else { + result[key] = { type: 'JSFunction', value: 'function() { /* lifecycle hook */ }' } + } + }) + return result +} + +function transformProps(props: any[]) { + return props.map((prop) => { + if (typeof prop === 'string') return { name: prop, type: 'any', default: undefined } + if (typeof prop === 'object') + return { + name: prop.name || 'unknownProp', + type: prop.type || 'any', + default: prop.default, + required: prop.required || false + } + return prop + }) +} + +// Generate an 8-char id with lowercase letters and digits +function generateId(): string { + let s = '' + while (s.length < 8) s += Math.random().toString(36).slice(2) + return s.slice(0, 8) +} + +// Recursively assign id to nodes with componentName +function assignComponentIds(node: any): void { + if (!node || typeof node !== 'object') return + if (typeof node.componentName === 'string') { + if (!node.id) node.id = generateId() + } + if (Array.isArray(node.children)) node.children.forEach(assignComponentIds) +} + +// Deeply sanitize all string values in the schema +function sanitizeSchemaStrings(obj: any): any { + if (obj === null || obj === undefined) return obj + if (typeof obj === 'string') return obj + if (Array.isArray(obj)) return obj.map((v) => sanitizeSchemaStrings(v)) + if (typeof obj === 'object') { + const out: any = Array.isArray(obj) ? [] : {} + Object.keys(obj).forEach((k) => { + out[k] = sanitizeSchemaStrings(obj[k]) + }) + return out + } + return obj +} + +export async function generateSchema(templateSchema: any[], scriptSchema: any, styleSchema: any, options: any = {}) { + const schema: any = { + componentName: 'Page', + fileName: options.fileName || 'UnnamedPage', + meta: { + name: options.fileName || 'UnnamedPage' + } + } + if (scriptSchema) { + if (scriptSchema.state) schema.state = transformState(scriptSchema.state) + if (scriptSchema.methods) schema.methods = transformMethods(scriptSchema.methods) + // only output computed when computed_flag is explicitly enabled + if (options.computed_flag === true && scriptSchema.computed) { + schema.computed = transformComputed(scriptSchema.computed) + } + if (scriptSchema.lifeCycles) schema.lifeCycles = transformLifeCycles(scriptSchema.lifeCycles) + if (scriptSchema.props && scriptSchema.props.length > 0) schema.props = transformProps(scriptSchema.props) + } + if (styleSchema && styleSchema.css) schema.css = styleSchema.css + if (templateSchema && templateSchema.length > 0) schema.children = templateSchema + // sanitize all strings to remove newlines in the final output + const sanitized = sanitizeSchemaStrings(schema) + // assign 8-char ids to all component nodes (including Page root) + assignComponentIds(sanitized) + return sanitized +} + +export function generateAppSchema(pageSchemas: any[], options: any = {}) { + return { + meta: { + name: options.name || 'Generated App', + description: options.description || 'App generated from Vue SFC files' + }, + i18n: options.i18n || { en_US: {}, zh_CN: {} }, + utils: options.utils || [], + dataSource: options.dataSource || { list: [] }, + globalState: options.globalState || [], + pageSchema: pageSchemas || [], + componentsMap: options.componentsMap || defaultComponentsMap + } +} diff --git a/packages/vue-to-dsl/src/index.d.ts b/packages/vue-to-dsl/src/index.d.ts new file mode 100644 index 0000000000..d1241e660c --- /dev/null +++ b/packages/vue-to-dsl/src/index.d.ts @@ -0,0 +1,155 @@ +declare module '@opentiny/tiny-engine-vue-to-dsl' { + export interface VueToSchemaOptions { + // 组件映射配置 + componentMap?: Record + // 是否保留注释 + preserveComments?: boolean + // 是否严格模式 + strictMode?: boolean + // 控制是否输出 computed 字段(默认 false) + computed_flag?: boolean + // 自定义解析器 + customParsers?: { + template?: TemplateParser + script?: ScriptParser + style?: StyleParser + } + } + + export interface TemplateParser { + parse(template: string, options?: any): TemplateSchema + } + + export interface ScriptParser { + parse(script: string, options?: any): ScriptSchema + } + + export interface StyleParser { + parse(style: string, options?: any): StyleSchema + } + + export interface TemplateSchema { + componentName: string + props?: Record + children?: TemplateSchema[] + condition?: string + loop?: string + key?: string + ref?: string + [key: string]: any + } + + export interface ScriptSchema { + state?: Record + methods?: Record + computed?: Record + lifeCycles?: Record + imports?: ImportInfo[] + props?: PropInfo[] + emits?: string[] + } + + export interface StyleSchema { + css: string + scoped?: boolean + lang?: string + } + + export interface ImportInfo { + source: string + specifiers: string[] + default?: string + } + + export interface PropInfo { + name: string + type?: string + default?: any + required?: boolean + } + + export interface PageSchema { + componentName: 'Page' + fileName: string + path: string + meta?: Record + state?: Record + methods?: Record + computed?: Record + lifeCycles?: Record + props?: PropInfo[] + css?: string + children?: TemplateSchema[] + } + + export interface ConvertResult { + schema: PageSchema + dependencies: string[] + errors: string[] + warnings: string[] + } + + export class VueToDslConverter { + constructor(options?: VueToSchemaOptions) + + /** + * 将Vue SFC文件内容转换为DSL Schema + */ + convertFromString(vueCode: string): Promise + + /** + * 将Vue SFC文件转换为DSL Schema + */ + convertFromFile(filePath: string): Promise + + /** + * 批量转换多个Vue文件 + */ + convertMultipleFiles(filePaths: string[]): Promise + } + + /** + * 解析Vue SFC文件 + */ + export function parseVueFile(filePath: string): Promise<{ + template?: string + script?: string + style?: string + scriptSetup?: string + }> + + /** + * 解析Vue SFC代码字符串 + */ + export function parseSFC(vueCode: string): { + template?: string + script?: string + style?: string + scriptSetup?: string + } + + /** + * 生成DSL Schema + */ + export function generateSchema( + template: string, + script: string, + style?: string, + options?: VueToSchemaOptions + ): Promise + + /** + * 解析模板 + */ + export function parseTemplate(template: string): TemplateSchema[] + + /** + * 解析脚本 + */ + export function parseScript(script: string): ScriptSchema + + /** + * 解析样式 + */ + export function parseStyle(style: string): StyleSchema +} diff --git a/packages/vue-to-dsl/src/index.ts b/packages/vue-to-dsl/src/index.ts new file mode 100644 index 0000000000..6075591824 --- /dev/null +++ b/packages/vue-to-dsl/src/index.ts @@ -0,0 +1,7 @@ +import './index.d.ts' + +export { VueToDslConverter } from './converter' +export { parseVueFile, parseSFC } from './parser' +export { generateSchema, generateAppSchema } from './generator' +export { parseTemplate, parseScript, parseStyle } from './parsers' +export * from './types/index' diff --git a/packages/vue-to-dsl/src/parser/index.ts b/packages/vue-to-dsl/src/parser/index.ts new file mode 100644 index 0000000000..40eb747e9d --- /dev/null +++ b/packages/vue-to-dsl/src/parser/index.ts @@ -0,0 +1,63 @@ +import { parse } from '@vue/compiler-sfc' +import fs from 'fs/promises' + +export function parseSFC(vueCode: string): any { + const { descriptor, errors } = parse(vueCode) + if (errors && (errors as any[]).length > 0) { + // eslint-disable-next-line no-console + console.warn('SFC parsing warnings:', errors) + } + + const result: any = {} + if (descriptor.template) { + result.template = descriptor.template.content + result.templateLang = descriptor.template.lang || 'html' + } + if (descriptor.scriptSetup) { + result.scriptSetup = descriptor.scriptSetup.content + result.scriptSetupLang = descriptor.scriptSetup.lang || 'js' + } + if (descriptor.script) { + result.script = descriptor.script.content + result.scriptLang = descriptor.script.lang || 'js' + } + if (descriptor.styles && descriptor.styles.length > 0) { + result.style = descriptor.styles.map((style) => style.content).join('\n\n') + result.styleBlocks = descriptor.styles.map((style) => ({ + content: style.content, + lang: style.lang || 'css', + scoped: style.scoped || false, + module: style.module || false + })) + } + if (descriptor.customBlocks && descriptor.customBlocks.length > 0) { + result.customBlocks = descriptor.customBlocks.map((block) => ({ + type: block.type, + content: (block as any).content, + attrs: (block as any).attrs + })) + } + return result +} + +export async function parseVueFile(filePath: string): Promise { + const content = await fs.readFile(filePath, 'utf-8') + return parseSFC(content) +} + +export function validateSFC(sfcResult: any): boolean { + return !!(sfcResult.template || sfcResult.script || sfcResult.scriptSetup) +} + +export function getSFCMeta(sfcResult: any) { + return { + hasTemplate: !!sfcResult.template, + hasScript: !!sfcResult.script, + hasScriptSetup: !!sfcResult.scriptSetup, + hasStyle: !!sfcResult.style, + templateLang: sfcResult.templateLang, + scriptLang: sfcResult.scriptLang || sfcResult.scriptSetupLang, + styleBlocks: sfcResult.styleBlocks || [], + customBlocks: sfcResult.customBlocks || [] + } +} diff --git a/packages/vue-to-dsl/src/parsers/index.ts b/packages/vue-to-dsl/src/parsers/index.ts new file mode 100644 index 0000000000..9b596456c2 --- /dev/null +++ b/packages/vue-to-dsl/src/parsers/index.ts @@ -0,0 +1,3 @@ +export { parseTemplate } from './templateParser' +export { parseScript } from './scriptParser' +export { parseStyle, parseCSSRules, extractCSSVariables, hasMediaQueries, extractMediaQueries } from './styleParser' diff --git a/packages/vue-to-dsl/src/parsers/scriptParser.ts b/packages/vue-to-dsl/src/parsers/scriptParser.ts new file mode 100644 index 0000000000..57abb20f15 --- /dev/null +++ b/packages/vue-to-dsl/src/parsers/scriptParser.ts @@ -0,0 +1,488 @@ +import { parse } from '@babel/parser' +import traverseModule from '@babel/traverse' +import * as t from '@babel/types' + +const traverse: any = (traverseModule as any)?.default ?? (traverseModule as any) + +const LIFECYCLE_HOOKS = [ + 'onMounted', + 'onUpdated', + 'onUnmounted', + 'onBeforeMount', + 'onBeforeUpdate', + 'onBeforeUnmount', + 'onActivated', + 'onDeactivated', + 'mounted', + 'updated', + 'unmounted', + 'beforeMount', + 'beforeUpdate', + 'beforeUnmount', + 'activated', + 'deactivated', + 'created', + 'beforeCreate', + 'destroyed', + 'beforeDestroy', + 'setup' +] + +function isVueReactiveCall(node: any, apiName: string) { + if (!t.isCallExpression(node)) return false + // direct call: reactive()/ref()/computed() + if (t.isIdentifier(node.callee) && node.callee.name === apiName) return true + // member call: vue.reactive()/Vue.ref()/anything.ref() + if (t.isMemberExpression(node.callee)) { + const callee = node.callee + const prop = callee.property + if (t.isIdentifier(prop) && prop.name === apiName) return true + } + return false +} + +function isLifecycleHook(name: string) { + return LIFECYCLE_HOOKS.includes(name) +} + +function getNodeValue(node: any): any { + if (t.isStringLiteral(node)) return node.value + if (t.isNumericLiteral(node)) return node.value + if (t.isBooleanLiteral(node)) return node.value + if (t.isNullLiteral(node)) return null + if (t.isUnaryExpression(node) && node.operator === '-' && t.isNumericLiteral(node.argument)) { + return -node.argument.value + } + if (t.isCallExpression(node)) { + let calleeStr = '' + if (t.isIdentifier(node.callee)) { + calleeStr = node.callee.name + } else if (t.isMemberExpression(node.callee)) { + const obj = node.callee.object as any + const prop = node.callee.property as any + const objStr = t.isIdentifier(obj) ? obj.name : '' + const propStr = t.isIdentifier(prop) ? prop.name : '' + if (objStr && propStr) calleeStr = `${objStr}.${propStr}` + } + const args = node.arguments.map((arg: any) => getNodeValue(arg)) + if (calleeStr) + return `${calleeStr}(${args.map((a: any) => (typeof a === 'string' ? `'${a}'` : String(a))).join(', ')})` + return 'undefined' + } + if (t.isObjectExpression(node)) { + const obj: Record = {} + node.properties.forEach((prop: any) => { + if (t.isObjectProperty(prop)) { + let keyName: string | null = null + if (t.isIdentifier(prop.key)) keyName = prop.key.name + else if (t.isStringLiteral(prop.key)) keyName = prop.key.value + else if (t.isNumericLiteral(prop.key)) keyName = String(prop.key.value) + if (keyName) obj[keyName] = getNodeValue(prop.value as any) + } + }) + return obj + } + if (t.isArrayExpression(node)) { + return node.elements.map((el: any) => (el ? getNodeValue(el) : null)) + } + return 'undefined' +} + +function getSource(node: any, source: string): string { + if (!node) return '' + const start = (node as any).start + const end = (node as any).end + if (typeof start === 'number' && typeof end === 'number') return source.slice(start, end) + return '' +} + +function arrowToFunctionString(name: string, node: t.ArrowFunctionExpression, source: string) { + const asyncStr = node.async ? 'async ' : '' + const params = node.params.map((p) => getSource(p, source)).join(', ') + if (t.isBlockStatement(node.body)) { + const body = getSource(node.body, source) + return `${asyncStr}function ${name}(${params}) ${body}` + } + const expr = getSource(node.body, source) + return `${asyncStr}function ${name}(${params}) { return ${expr}; }` +} + +function functionExpressionToNamedFunctionString( + name: string, + node: t.FunctionExpression | t.ObjectMethod, + source: string +) { + const asyncStr = (node as any).async ? 'async ' : '' + const params = (node as any).params.map((p: any) => getSource(p, source)).join(', ') + const body = getSource((node as any).body, source) + return `${asyncStr}function ${name}(${params}) ${body}` +} + +// ---- setup 专属逻辑的小分支封装(共享生命周期处理主干)---- +const isSetupName = (name: string) => name === 'setup' + +function setLifecycleEntry(result: any, name: string, code: string, opts: { noOverride?: boolean } = {}) { + if (opts.noOverride && result.lifeCycles[name]) return + result.lifeCycles[name] = { type: 'lifecycle', value: code || (name ? `function ${name}(){}` : 'function() {}') } +} + +function setMethodEntry(result: any, name: string, code: string) { + result.methods[name] = { type: 'function', value: code || `function ${name}(){}` } +} + +function routeFunctionLikeByName(result: any, name: string, code: string) { + if (isSetupName(name)) setLifecycleEntry(result, name, code) + else setMethodEntry(result, name, code) +} + +// Helpers to reduce duplication when handling variable declarators in + + diff --git a/packages/vue-to-dsl/test/full/input/appdemo01/package.json b/packages/vue-to-dsl/test/full/input/appdemo01/package.json new file mode 100644 index 0000000000..6f3161e7ca --- /dev/null +++ b/packages/vue-to-dsl/test/full/input/appdemo01/package.json @@ -0,0 +1,29 @@ +{ + "name": "portal-app", + "version": "1.0.0", + "scripts": { + "dev": "vite", + "build": "vite build", + "preview": "vite preview" + }, + "type": "module", + "main": "dist/index.js", + "module": "dist/index.js", + "dependencies": { + "@opentiny/tiny-engine-i18n-host": "^1.0.0", + "@opentiny/vue": "3.24.0", + "@opentiny/vue-icon": "3.24.0", + "axios": "latest", + "axios-mock-adapter": "^1.19.0", + "vue": "^3.3.9", + "vue-i18n": "^9.2.0-beta.3", + "vue-router": "^4.2.5", + "pinia": "^2.1.7", + "@opentiny/tiny-engine-builtin-component": "^2.0.0" + }, + "devDependencies": { + "@vitejs/plugin-vue": "^5.1.2", + "@vitejs/plugin-vue-jsx": "^4.0.1", + "vite": "^5.4.2" + } +} diff --git a/packages/vue-to-dsl/test/full/input/appdemo01/public/favicon.ico b/packages/vue-to-dsl/test/full/input/appdemo01/public/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..6271b2d8129adb150b3e8c51956cbe9e1c045c55 GIT binary patch literal 12996 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 O(bG28s?>P$?Ee7CusRa} literal 0 HcmV?d00001 diff --git a/packages/vue-to-dsl/test/full/input/appdemo01/src/App.vue b/packages/vue-to-dsl/test/full/input/appdemo01/src/App.vue new file mode 100644 index 0000000000..72b6032dea --- /dev/null +++ b/packages/vue-to-dsl/test/full/input/appdemo01/src/App.vue @@ -0,0 +1,11 @@ + + + diff --git a/packages/vue-to-dsl/test/full/input/appdemo01/src/http/axios.js b/packages/vue-to-dsl/test/full/input/appdemo01/src/http/axios.js new file mode 100644 index 0000000000..3654c619b4 --- /dev/null +++ b/packages/vue-to-dsl/test/full/input/appdemo01/src/http/axios.js @@ -0,0 +1,143 @@ +/** + * 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 axios from 'axios' +import MockAdapter from 'axios-mock-adapter' + +export default (config) => { + const instance = axios.create(config) + const defaults = {} + let mock + + if (typeof MockAdapter.prototype.proxy === 'undefined') { + MockAdapter.prototype.proxy = function ({ url, config = {}, proxy, response, handleData } = {}) { + // eslint-disable-next-line @typescript-eslint/no-this-alias + let stream = this + const request = (proxy, any) => { + return (setting) => { + return new Promise((resolve) => { + config.responseType = 'json' + axios + .get(any ? proxy + setting.url + '.json' : proxy, config) + .then(({ data }) => { + if (typeof handleData === 'function') { + data = handleData.call(null, data, setting) + } + resolve([200, data]) + }) + .catch((error) => { + resolve([error.response.status, error.response.data]) + }) + }) + } + } + + if (url === '*' && proxy && typeof proxy === 'string') { + stream = proxy === '*' ? this.onAny().passThrough() : this.onAny().reply(request(proxy, true)) + } else { + if (proxy && typeof proxy === 'string') { + stream = this.onAny(url).reply(request(proxy)) + } else if (typeof response === 'function') { + stream = this.onAny(url).reply(response) + } + } + + return stream + } + } + + return { + request(config) { + return instance(config) + }, + get(url, config) { + return instance.get(url, config) + }, + delete(url, config) { + return instance.delete(url, config) + }, + head(url, config) { + return instance.head(url, config) + }, + post(url, data, config) { + return instance.post(url, data, config) + }, + put(url, data, config) { + return instance.put(url, data, config) + }, + patch(url, data, config) { + return instance.patch(url, data, config) + }, + all(iterable) { + return axios.all(iterable) + }, + spread(callback) { + return axios.spread(callback) + }, + defaults(key, value) { + if (key && typeof key === 'string') { + if (typeof value === 'undefined') { + return instance.defaults[key] + } + instance.defaults[key] = value + defaults[key] = value + } else { + return instance.defaults + } + }, + defaultSettings() { + return defaults + }, + interceptors: { + request: { + use(fnHandle, fnError) { + return instance.interceptors.request.use(fnHandle, fnError) + }, + eject(id) { + return instance.interceptors.request.eject(id) + } + }, + response: { + use(fnHandle, fnError) { + return instance.interceptors.response.use(fnHandle, fnError) + }, + eject(id) { + return instance.interceptors.response.eject(id) + } + } + }, + mock(config) { + if (!mock) { + mock = new MockAdapter(instance) + } + + if (Array.isArray(config)) { + config.forEach((item) => { + mock.proxy(item) + }) + } + + return mock + }, + disableMock() { + if (mock) { + mock.restore() + } + mock = undefined + }, + isMock() { + return typeof mock !== 'undefined' + }, + CancelToken: axios.CancelToken, + isCancel: axios.isCancel + } +} diff --git a/packages/vue-to-dsl/test/full/input/appdemo01/src/http/config.js b/packages/vue-to-dsl/test/full/input/appdemo01/src/http/config.js new file mode 100644 index 0000000000..cfa3714e17 --- /dev/null +++ b/packages/vue-to-dsl/test/full/input/appdemo01/src/http/config.js @@ -0,0 +1,15 @@ +/** + * 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. + * + */ + +export default { + withCredentials: false +} diff --git a/packages/vue-to-dsl/test/full/input/appdemo01/src/http/index.js b/packages/vue-to-dsl/test/full/input/appdemo01/src/http/index.js new file mode 100644 index 0000000000..b0a08546a6 --- /dev/null +++ b/packages/vue-to-dsl/test/full/input/appdemo01/src/http/index.js @@ -0,0 +1,27 @@ +/** + * 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 axios from './axios' +import config from './config' + +export default (dataHandler) => { + const http = axios(config) + + http.interceptors.response.use(dataHandler, (error) => { + const response = error.response + if (response.status === 403 && response.headers && response.headers['x-login-url']) { + // TODO 处理无权限时,重新登录再发送请求 + } + }) + + return http +} diff --git a/packages/vue-to-dsl/test/full/input/appdemo01/src/i18n/en_US.json b/packages/vue-to-dsl/test/full/input/appdemo01/src/i18n/en_US.json new file mode 100644 index 0000000000..be5c684e5e --- /dev/null +++ b/packages/vue-to-dsl/test/full/input/appdemo01/src/i18n/en_US.json @@ -0,0 +1,25 @@ +{ + "lowcode.c257d5e8": "search", + "lowcode.61c8ac8c": "dsdsa", + "lowcode.f53187a0": "test", + "lowcode.97ad00dd": "createMaterial", + "lowcode.61dcef52": "sadasda", + "lowcode.45f4c42a": "gfdgfd", + "lowcode.c6f5a652": "fsdafds", + "lowcode.34923432": "fdsafds", + "lowcode.6534943e": "fdsafdsa", + "lowcode.44252642": "aaaa", + "lowcode.2a743651": "fdsaf", + "lowcode.24315357": "fsdafds", + "lowcode.44621691": "sd", + "lowcode.65636226": "fdsfsd", + "lowcode.6426a4e2": "fdsafsd", + "lowcode.e41c6636": "aa", + "lowcode.51c23164": "aa", + "lowcode.17245b46": "aa", + "lowcode.4573143c": "a", + "lowcode.56432442": "aa", + "lowcode.33566643": "aa", + "lowcode.565128f3": "aa", + "lowcode.56643835": "aa" +} diff --git a/packages/vue-to-dsl/test/full/input/appdemo01/src/i18n/index.js b/packages/vue-to-dsl/test/full/input/appdemo01/src/i18n/index.js new file mode 100644 index 0000000000..f6c510b279 --- /dev/null +++ b/packages/vue-to-dsl/test/full/input/appdemo01/src/i18n/index.js @@ -0,0 +1,9 @@ +import i18n from '@opentiny/tiny-engine-i18n-host' +import lowcode from '../lowcodeConfig/lowcode' +import locale from './locale.js' + +i18n.lowcode = lowcode +i18n.global.mergeLocaleMessage('en_US', locale.en_US) +i18n.global.mergeLocaleMessage('zh_CN', locale.zh_CN) + +export default i18n diff --git a/packages/vue-to-dsl/test/full/input/appdemo01/src/i18n/locale.js b/packages/vue-to-dsl/test/full/input/appdemo01/src/i18n/locale.js new file mode 100644 index 0000000000..75308fc752 --- /dev/null +++ b/packages/vue-to-dsl/test/full/input/appdemo01/src/i18n/locale.js @@ -0,0 +1,4 @@ +import en_US from './en_US.json' +import zh_CN from './zh_CN.json' + +export default { en_US, zh_CN } diff --git a/packages/vue-to-dsl/test/full/input/appdemo01/src/i18n/zh_CN.json b/packages/vue-to-dsl/test/full/input/appdemo01/src/i18n/zh_CN.json new file mode 100644 index 0000000000..59357fdfcc --- /dev/null +++ b/packages/vue-to-dsl/test/full/input/appdemo01/src/i18n/zh_CN.json @@ -0,0 +1,26 @@ +{ + "lowcode.c257d5e8": "查询", + "lowcode.61c8ac8c": "地方", + "lowcode.f53187a0": "测试", + "lowcode.97ad00dd": "创建物料资产包", + "lowcode.61dcef52": "terterere", + "lowcode.45f4c42a": "gdfgdf", + "lowcode.c6f5a652": "fsdaf", + "lowcode.34923432": "fdsafdsa", + "lowcode.48521e45": "fdsfds", + "lowcode.6534943e": "fdsafds", + "lowcode.44252642": "fdsafds", + "lowcode.2a743651": "sda", + "lowcode.24315357": "fdsafds", + "lowcode.44621691": "fdsafsd", + "lowcode.65636226": "fdsaf", + "lowcode.6426a4e2": "sd", + "lowcode.e41c6636": "aa", + "lowcode.51c23164": "aa", + "lowcode.17245b46": "aa", + "lowcode.4573143c": "aa", + "lowcode.56432442": "aa", + "lowcode.33566643": "aa", + "lowcode.565128f3": "aa", + "lowcode.56643835": "aa" +} diff --git a/packages/vue-to-dsl/test/full/input/appdemo01/src/lowcodeConfig/bridge.js b/packages/vue-to-dsl/test/full/input/appdemo01/src/lowcodeConfig/bridge.js new file mode 100644 index 0000000000..7a19e4a116 --- /dev/null +++ b/packages/vue-to-dsl/test/full/input/appdemo01/src/lowcodeConfig/bridge.js @@ -0,0 +1,13 @@ +/** + * 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. + * + */ + +export default () => {} diff --git a/packages/vue-to-dsl/test/full/input/appdemo01/src/lowcodeConfig/dataSource.js b/packages/vue-to-dsl/test/full/input/appdemo01/src/lowcodeConfig/dataSource.js new file mode 100644 index 0000000000..f82f146bfc --- /dev/null +++ b/packages/vue-to-dsl/test/full/input/appdemo01/src/lowcodeConfig/dataSource.js @@ -0,0 +1,104 @@ +/** + * 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 useHttp from '../http' +import dataSources from './dataSource.json' + +const dataSourceMap = {} + +// 暂时使用 eval 解析 JSON 数据里的函数 +const createFn = (fnContent) => { + return (...args) => { + // eslint-disable-next-line no-eval + window.eval('var fn = ' + fnContent) + // eslint-disable-next-line no-undef + return fn.apply(this, args) + } +} + +const globalDataHandle = dataSources.dataHandler ? createFn(dataSources.dataHandler.value) : (res) => res + +const load = (http, options, dataSource, shouldFetch) => (params, customUrl) => { + // 如果没有配置远程请求,则直接返回静态数据,返回前可能会有全局数据处理 + if (!options) { + return globalDataHandle(dataSource.config.data) + } + + if (!shouldFetch()) { + return + } + + dataSource.status = 'loading' + + const { method, uri: url, params: defaultParams, timeout, headers } = options + const config = { method, url, headers, timeout } + + const data = params || defaultParams + + config.url = customUrl || config.url + + if (method.toLowerCase() === 'get') { + config.params = data + } else { + config.data = data + } + + return http.request(config) +} + +dataSources.list.forEach((config) => { + const http = useHttp(globalDataHandle) + const dataSource = { config } + + dataSourceMap[config.name] = dataSource + + const shouldFetch = config.shouldFetch?.value ? createFn(config.shouldFetch.value) : () => true + const willFetch = config.willFetch?.value ? createFn(config.willFetch.value) : (options) => options + + const dataHandler = (res) => { + const data = config.dataHandler?.value ? createFn(config.dataHandler.value)(res) : res + dataSource.status = 'loaded' + dataSource.data = data + return data + } + + const errorHandler = (error) => { + if (config.errorHandler?.value) { + createFn(config.errorHandler.value)(error) + } + dataSource.status = 'error' + dataSource.error = error + } + + http.interceptors.request.use(willFetch, errorHandler) + http.interceptors.response.use(dataHandler, errorHandler) + + if (import.meta.env.VITE_APP_MOCK === 'mock') { + http.mock([ + { + url: config.options?.uri, + response() { + return Promise.resolve([200, { data: config.data }]) + } + }, + { + url: '*', + proxy: '*' + } + ]) + } + + dataSource.status = 'init' + dataSource.load = load(http, config.options, dataSource, shouldFetch) +}) + +export default dataSourceMap diff --git a/packages/vue-to-dsl/test/full/input/appdemo01/src/lowcodeConfig/dataSource.json b/packages/vue-to-dsl/test/full/input/appdemo01/src/lowcodeConfig/dataSource.json new file mode 100644 index 0000000000..73ff9cb058 --- /dev/null +++ b/packages/vue-to-dsl/test/full/input/appdemo01/src/lowcodeConfig/dataSource.json @@ -0,0 +1,632 @@ +{ + "list": [ + { + "id": 132, + "name": "getAllComponent", + "data": [], + "type": "array" + }, + { + "id": 133, + "name": "getAllList", + "columns": [ + { + "name": "test", + "title": "测试", + "field": "test", + "type": "string", + "format": {} + }, + { + "name": "test1", + "title": "测试1", + "field": "test1", + "type": "string", + "format": {} + } + ], + "type": "array", + "data": [ + { + "test": "test1", + "test1": "test1", + "_id": "341efc48" + }, + { + "test": "test2", + "test1": "test1", + "_id": "b86b516c" + }, + { + "test": "test3", + "test1": "test1", + "_id": "f680cd78" + } + ], + "options": { + "uri": "", + "method": "GET" + }, + "dataHandler": { + "type": "JSFunction", + "value": "function dataHandler(data) { \n return data \n}" + }, + "willFetch": { + "type": "JSFunction", + "value": "function willFetch(option) {\n return option \n}" + }, + "shouldFetch": { + "type": "JSFunction", + "value": "function shouldFetch(option) {\n return true \n}" + }, + "errorHandler": { + "type": "JSFunction", + "value": "function errorHandler(err) {}" + } + }, + { + "id": 135, + "name": "getAllMaterialList", + "columns": [ + { + "name": "id", + "title": "id", + "field": "id", + "type": "string", + "format": {} + }, + { + "name": "name", + "title": "name", + "field": "name", + "type": "string", + "format": {} + }, + { + "name": "framework", + "title": "framework", + "field": "framework", + "type": "string", + "format": { + "required": true + } + }, + { + "name": "components", + "title": "components", + "field": "components", + "type": "string", + "format": {} + }, + { + "name": "content", + "title": "content", + "field": "content", + "type": "string", + "format": {} + }, + { + "name": "url", + "title": "url", + "field": "url", + "type": "string", + "format": {} + }, + { + "name": "published_at", + "title": "published_at", + "field": "published_at", + "type": "string", + "format": {} + }, + { + "name": "created_at", + "title": "created_at", + "field": "created_at", + "type": "string", + "format": {} + }, + { + "name": "updated_at", + "title": "updated_at", + "field": "updated_at", + "type": "string", + "format": {} + }, + { + "name": "published", + "title": "published", + "field": "published", + "type": "string", + "format": {} + }, + { + "name": "last_build_info", + "title": "last_build_info", + "field": "last_build_info", + "type": "string", + "format": {} + }, + { + "name": "tenant", + "title": "tenant", + "field": "tenant", + "type": "string", + "format": {} + }, + { + "name": "version", + "title": "version", + "field": "version", + "type": "string", + "format": {} + }, + { + "name": "description", + "title": "description", + "field": "description", + "type": "string", + "format": {} + } + ], + "type": "array", + "data": [ + { + "id": "f37123ec", + "url": "", + "name": "ng-material", + "tenant": "", + "content": "", + "version": "1.0.0", + "framework": "Angular", + "published": "", + "components": "", + "created_at": "2021-11-02T11:32:22.000Z", + "updated_at": "2021-11-02T11:32:22.000Z", + "description": "angular组件库物料", + "published_at": "2021-11-02T11:32:22.000Z", + "last_build_info": "", + "_id": "2a23e653" + }, + { + "id": "f37123ec", + "url": "", + "name": "ng-material", + "tenant": "", + "content": "", + "version": "1.0.0", + "framework": "Angular", + "published": "", + "components": "", + "created_at": "2021-11-02T11:32:22.000Z", + "updated_at": "2021-11-02T11:32:22.000Z", + "description": "angular组件库物料", + "published_at": "2021-11-02T11:32:22.000Z", + "last_build_info": "", + "_id": "06b253be" + }, + { + "id": "f37123ec", + "url": "", + "name": "ng-material", + "tenant": "", + "content": "", + "version": "1.0.0", + "framework": "Angular", + "published": "", + "components": "", + "created_at": "2021-11-02T11:32:22.000Z", + "updated_at": "2021-11-02T11:32:22.000Z", + "description": "angular组件库物料", + "published_at": "2021-11-02T11:32:22.000Z", + "last_build_info": "", + "_id": "c55a41ed" + }, + { + "id": "f37123ec", + "url": "", + "name": "ng-material", + "tenant": "", + "content": "", + "version": "1.0.0", + "framework": "Angular", + "published": "", + "components": "", + "created_at": "2021-11-02T11:32:22.000Z", + "updated_at": "2021-11-02T11:32:22.000Z", + "description": "angular组件库物料", + "published_at": "2021-11-02T11:32:22.000Z", + "last_build_info": "", + "_id": "f37123ec" + }, + { + "id": "7a63c1a2", + "url": "", + "name": "tiny-vue", + "tenant": "", + "content": "Tiny Vue物料", + "version": "1.0.0", + "framework": "Vue", + "published": "", + "components": "", + "created_at": "", + "updated_at": "", + "description": "Tiny Vue物料", + "published_at": "", + "last_build_info": "", + "_id": "7a63c1a2" + } + ], + "options": { + "uri": "", + "method": "GET" + }, + "willFetch": { + "type": "JSFunction", + "value": "function willFetch(option) {\n return option \n}" + }, + "dataHandler": { + "type": "JSFunction", + "value": "function dataHandler(data) { \n return data \n}" + }, + "shouldFetch": { + "type": "JSFunction", + "value": "function shouldFetch(option) {\n return true \n}" + }, + "errorHandler": { + "type": "JSFunction", + "value": "function errorHandler(err) {}" + } + }, + { + "id": 139, + "name": "treedata", + "data": [ + { + "label": "level111", + "value": "111", + "id": "f6609643", + "pid": "", + "_RID": "row_4" + }, + { + "label": "level1-son", + "value": "111-1", + "id": "af1f937f", + "pid": "f6609643", + "_RID": "row_5" + }, + { + "label": "level222", + "value": "222", + "id": "28e3709c", + "pid": "", + "_RID": "row_6" + }, + { + "label": "level2-son", + "value": "222-1", + "id": "6b571bef", + "pid": "28e3709c", + "_RID": "row_5" + }, + { + "id": "6317c2cc", + "pid": "fdfa", + "label": "fsdfaa", + "value": "fsadf", + "_RID": "row_6" + }, + { + "id": "9cce369f", + "pid": "test", + "label": "test1", + "value": "001" + } + ], + "type": "tree" + }, + { + "id": 150, + "name": "componentList", + "data": [ + { + "_RID": "row_1", + "name": "表单", + "isSelected": "true", + "description": "由按钮、输入框、选择器、单选框、多选框等控件组成,用以收集、校验、提交数据" + }, + { + "name": "按钮", + "isSelected": "false", + "description": "常用的操作按钮,提供包括默认按钮、图标按钮、图片按钮、下拉按钮等类型" + }, + { + "id": "490f8a00", + "_RID": "row_3", + "name": "表单项", + "framework": "", + "materials": "", + "description": "Form 组件下的 FormItem 配置" + }, + { + "id": "c259b8b3", + "_RID": "row_4", + "name": "开关", + "framework": "", + "materials": "", + "description": "关闭或打开" + }, + { + "id": "083ed9c7", + "_RID": "row_5", + "name": "互斥按钮组", + "framework": "", + "materials": "", + "description": "以按钮组的方式出现,常用于多项类似操作" + }, + { + "id": "09136cea", + "_RID": "row_6", + "name": "提示框", + "framework": "", + "materials": "", + "description": "Popover可通过对一个触发源操作触发弹出框,支持自定义弹出内容,延迟触发和渐变动画" + }, + { + "id": "a63b57d5", + "_RID": "row_7", + "name": "文字提示框", + "framework": "", + "materials": "", + "description": "动态显示提示信息,一般通过鼠标事件进行响应;提供 warning、error、info、success 四种类型显示不同类别的信" + }, + { + "id": "a0f6e8a3", + "_RID": "row_8", + "name": "树", + "framework": "", + "materials": "", + "description": "可进行展示有父子层级的数据,支持选择,异步加载等功能。但不推荐用它来展示菜单,展示菜单推荐使用树菜单" + }, + { + "id": "d1aa18fc", + "_RID": "row_9", + "name": "分页", + "framework": "", + "materials": "", + "description": "当数据量过多时,使用分页分解数据,常用于 Grid 和 Repeater 组件" + }, + { + "id": "ca49cc52", + "_RID": "row_10", + "name": "表格", + "framework": "", + "materials": "", + "description": "提供了非常强大数据表格功能,可以展示数据列表,可以对数据列表进行选择、编辑等" + }, + { + "id": "4e20ecc9", + "name": "搜索框", + "framework": "", + "materials": "", + "description": "指定条件对象进行搜索数据" + }, + { + "id": "6b093ee5", + "name": "折叠面板", + "framework": "", + "materials": "", + "description": "内容区可指定动态页面或自定义 html 等,支持展开收起操作" + }, + { + "id": "0a09abc0", + "name": "对话框", + "framework": "", + "materials": "", + "description": "模态对话框,在浮层中显示,引导用户进行相关操作" + }, + { + "id": "f814b901", + "name": "标签页签项", + "framework": "", + "materials": "", + "description": "tab页签" + }, + { + "id": "c5ae797c", + "name": "单选", + "framework": "", + "materials": "", + "description": "用于配置不同场景的选项,在一组备选项中进行单选" + }, + { + "id": "33d0c590", + "_RID": "row_13", + "name": "弹出编辑", + "framework": "", + "materials": "", + "description": "该组件只能在弹出的面板中选择数据,不能手动输入数据;弹出面板中显示为 Tree 组件或者 Grid 组件" + }, + { + "id": "16711dfa", + "_RID": "row_14", + "name": "下拉框", + "framework": "", + "materials": "", + "description": "Select 选择器是一种通过点击弹出下拉列表展示数据并进行选择的 UI 组件" + }, + { + "id": "a9fd190a", + "_RID": "row_15", + "name": "折叠面板项", + "framework": "", + "materials": "", + "description": "内容区可指定动态页面或自定义 html 等,支持展开收起操作" + }, + { + "id": "a7dfa9ec", + "_RID": "row_16", + "name": "复选框", + "framework": "", + "materials": "", + "description": "用于配置不同场景的选项,提供用户可在一组选项中进行多选" + }, + { + "id": "d4bb8330", + "name": "输入框", + "framework": "", + "materials": "", + "description": "通过鼠标或键盘输入字符" + }, + { + "id": "ced3dc83", + "name": "时间线", + "framework": "", + "materials": "", + "description": "时间线" + } + ], + "type": "array", + "columns": [ + { + "name": "name", + "type": "string", + "field": "name", + "title": "name", + "format": { + "max": 0, + "min": 0, + "dateTime": false, + "required": false, + "stringType": "" + } + }, + { + "name": "description", + "type": "string", + "field": "description", + "title": "description", + "format": { + "max": 0, + "min": 0, + "dateTime": false, + "required": false, + "stringType": "" + } + }, + { + "name": "isSelected", + "type": "string", + "field": "isSelected", + "title": "isSelected", + "format": { + "max": 0, + "min": 0, + "dateTime": false, + "required": false, + "stringType": "" + } + } + ], + "options": { + "uri": "http://localhost:9090/assets/json/bundle.json", + "method": "GET" + }, + "willFetch": { + "type": "JSFunction", + "value": "function willFetch(option) {\n return option \n}" + }, + "dataHandler": { + "type": "JSFunction", + "value": "function dataHandler(data) { \n return data \n}" + }, + "shouldFetch": { + "type": "JSFunction", + "value": "function shouldFetch(option) {\n return true \n}" + }, + "errorHandler": { + "type": "JSFunction", + "value": "function errorHandler(err) {}" + } + }, + { + "id": 151, + "name": "selectedComponents", + "columns": [ + { + "name": "name", + "title": "name", + "field": "name", + "type": "string", + "format": { + "required": false, + "stringType": "", + "min": 0, + "max": 0, + "dateTime": false + } + }, + { + "name": "description", + "title": "description", + "field": "description", + "type": "string", + "format": { + "required": false, + "stringType": "", + "min": 0, + "max": 0, + "dateTime": false + } + }, + { + "name": "isSelected", + "title": "isSelected", + "field": "isSelected", + "type": "string", + "format": { + "required": false, + "stringType": "", + "min": 0, + "max": 0, + "dateTime": false + } + } + ], + "type": "array", + "data": [ + { + "name": "标签页", + "description": "分隔内容上有关联但属于不同类别的数据集合", + "isSelected": "true", + "_RID": "row_2" + }, + { + "name": "布局列", + "description": "列配置信息", + "isSelected": "true", + "id": "76a7080a", + "_RID": "row_4" + }, + { + "name": "日期选择器", + "description": "用于设置/选择日期,包括年月/年月日/年月日时分/年月日时分秒日期格式", + "isSelected": "true", + "id": "76b20d73", + "_RID": "row_1" + }, + { + "name": "走马灯", + "description": "常用于一组图片或卡片轮播,当内容空间不足时,可以用走马灯的形式进行收纳,进行轮播展现", + "isSelected": "true", + "id": "4c884c3d" + } + ] + } + ], + "dataHandler": { + "type": "JSFunction", + "value": "function dataHanlder(res){\n return res;\n}" + } +} diff --git a/packages/vue-to-dsl/test/full/input/appdemo01/src/lowcodeConfig/lowcode.js b/packages/vue-to-dsl/test/full/input/appdemo01/src/lowcodeConfig/lowcode.js new file mode 100644 index 0000000000..29da8186b5 --- /dev/null +++ b/packages/vue-to-dsl/test/full/input/appdemo01/src/lowcodeConfig/lowcode.js @@ -0,0 +1,86 @@ +/** + * 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 { getCurrentInstance, nextTick, provide, inject } from 'vue' +import { useRouter, useRoute } from 'vue-router' +import { I18nInjectionKey } from 'vue-i18n' +import dataSourceMap from './dataSource' +import * as utils from '../utils' +import * as bridge from './bridge' +import { useStores } from './store' + +export const lowcodeWrap = (props, context) => { + const global = {} + const instance = getCurrentInstance() + const router = useRouter() + const route = useRoute() + const { t, locale } = inject(I18nInjectionKey).global + const emit = context.emit + const ref = (ref) => instance.refs[ref] + + const setState = (newState, callback) => { + Object.assign(global.state, newState) + nextTick(() => callback.apply(global)) + } + + const getLocale = () => locale.value + const setLocale = (val) => { + locale.value = val + } + + const location = () => window.location + const history = () => window.history + + Object.defineProperties(global, { + props: { get: () => props }, + emit: { get: () => emit }, + setState: { get: () => setState }, + router: { get: () => router }, + route: { get: () => route }, + i18n: { get: () => t }, + getLocale: { get: () => getLocale }, + setLocale: { get: () => setLocale }, + location: { get: location }, + history: { get: history }, + utils: { get: () => utils }, + bridge: { get: () => bridge }, + dataSourceMap: { get: () => dataSourceMap }, + $: { get: () => ref } + }) + + const wrap = (fn) => { + if (typeof fn === 'function') { + return (...args) => fn.apply(global, args) + } + + Object.entries(fn).forEach(([name, value]) => { + Object.defineProperty(global, name, { + get: () => value + }) + }) + + fn.t = t + + return fn + } + + return wrap +} + +export default () => { + const i18n = inject(I18nInjectionKey) + provide(I18nInjectionKey, i18n) + + const stores = useStores() + + return { t: i18n.global.t, lowcodeWrap, stores } +} diff --git a/packages/vue-to-dsl/test/full/input/appdemo01/src/lowcodeConfig/store.js b/packages/vue-to-dsl/test/full/input/appdemo01/src/lowcodeConfig/store.js new file mode 100644 index 0000000000..f7f39c7a84 --- /dev/null +++ b/packages/vue-to-dsl/test/full/input/appdemo01/src/lowcodeConfig/store.js @@ -0,0 +1,13 @@ +import * as useDefinedStores from '@/stores' + +const useStores = () => { + const stores = {} + + Object.values({ ...useDefinedStores }).forEach((store) => { + stores[store.$id] = store() + }) + + return stores +} + +export { useStores } diff --git a/packages/vue-to-dsl/test/full/input/appdemo01/src/main.js b/packages/vue-to-dsl/test/full/input/appdemo01/src/main.js new file mode 100644 index 0000000000..c4574461b3 --- /dev/null +++ b/packages/vue-to-dsl/test/full/input/appdemo01/src/main.js @@ -0,0 +1,20 @@ +/** + * 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 { createApp } from 'vue' +import router from './router' +import { createPinia } from 'pinia' +import App from './App.vue' + +const pinia = createPinia() + +createApp(App).use(pinia).use(router).mount('#app') diff --git a/packages/vue-to-dsl/test/full/input/appdemo01/src/router/index.js b/packages/vue-to-dsl/test/full/input/appdemo01/src/router/index.js new file mode 100644 index 0000000000..a06668a4c0 --- /dev/null +++ b/packages/vue-to-dsl/test/full/input/appdemo01/src/router/index.js @@ -0,0 +1,37 @@ +import { createRouter, createWebHashHistory } from 'vue-router' +const routes = [ + { + path: '/', + children: [ + { + name: '1', + path: 'CreateVm', + component: () => import('@/views/CreateVm.vue'), + children: [] + }, + { + name: '1GhxcwoNeestd4aI', + path: 'Demopage', + component: () => import('@/views/DemoPage.vue'), + children: [] + }, + { + name: 'MQSQpz7noWlTRnse', + path: 'Lifecycle', + component: () => import('@/views/LifeCyclePage.vue'), + children: [] + }, + { + name: 'mPX398RIysZI3CRG', + path: 'UntitledA', + component: () => import('@/views/UntitledA.vue'), + children: [] + } + ] + } +] + +export default createRouter({ + history: createWebHashHistory(), + routes +}) diff --git a/packages/vue-to-dsl/test/full/input/appdemo01/src/stores/index.js b/packages/vue-to-dsl/test/full/input/appdemo01/src/stores/index.js new file mode 100644 index 0000000000..380fa26bf6 --- /dev/null +++ b/packages/vue-to-dsl/test/full/input/appdemo01/src/stores/index.js @@ -0,0 +1 @@ +export { testState } from './testState' diff --git a/packages/vue-to-dsl/test/full/input/appdemo01/src/stores/testState.js b/packages/vue-to-dsl/test/full/input/appdemo01/src/stores/testState.js new file mode 100644 index 0000000000..c2312435e9 --- /dev/null +++ b/packages/vue-to-dsl/test/full/input/appdemo01/src/stores/testState.js @@ -0,0 +1,27 @@ +import { defineStore } from 'pinia' +export const testState = defineStore({ + id: 'testState', + state: () => ({ + name: 'testName', + license: '', + age: 18, + food: ['apple', 'orange', 'banana', 19], + desc: { description: 'hello world', money: 100, other: '', rest: ['a', 'b', 'c', 20] } + }), + getters: { + getAge: function getAge() { + return this.age + }, + getName: function getName() { + return this.name + } + }, + actions: { + setAge: function setAge(age) { + this.age = age + }, + setName: function setName(name) { + this.name = name + } + } +}) diff --git a/packages/vue-to-dsl/test/full/input/appdemo01/src/utils.js b/packages/vue-to-dsl/test/full/input/appdemo01/src/utils.js new file mode 100644 index 0000000000..42009b621d --- /dev/null +++ b/packages/vue-to-dsl/test/full/input/appdemo01/src/utils.js @@ -0,0 +1,13 @@ +import axios from 'axios' +import { Button } from '@opentiny/vue' +import { NavMenu } from '@opentiny/vue' +import { Modal } from '@opentiny/vue' +import { Pager } from '@opentiny/vue' +const npm = '' +const test = function test() { + return 'test' +} +const util = function util() { + console.log(321) +} +export { axios, Button, NavMenu, Modal, npm, Pager, test, util } diff --git a/packages/vue-to-dsl/test/full/input/appdemo01/src/views/DemoPage.vue b/packages/vue-to-dsl/test/full/input/appdemo01/src/views/DemoPage.vue new file mode 100644 index 0000000000..cecb287f05 --- /dev/null +++ b/packages/vue-to-dsl/test/full/input/appdemo01/src/views/DemoPage.vue @@ -0,0 +1,27 @@ + + + + diff --git a/packages/vue-to-dsl/test/full/input/appdemo01/src/views/LifeCyclePage.vue b/packages/vue-to-dsl/test/full/input/appdemo01/src/views/LifeCyclePage.vue new file mode 100644 index 0000000000..8be8b629bf --- /dev/null +++ b/packages/vue-to-dsl/test/full/input/appdemo01/src/views/LifeCyclePage.vue @@ -0,0 +1,60 @@ + + + + diff --git a/packages/vue-to-dsl/test/full/input/appdemo01/src/views/UntitledA.vue b/packages/vue-to-dsl/test/full/input/appdemo01/src/views/UntitledA.vue new file mode 100644 index 0000000000..c9b57ed7cc --- /dev/null +++ b/packages/vue-to-dsl/test/full/input/appdemo01/src/views/UntitledA.vue @@ -0,0 +1,38 @@ + + + + diff --git a/packages/vue-to-dsl/test/full/input/appdemo01/src/views/createVm.vue b/packages/vue-to-dsl/test/full/input/appdemo01/src/views/createVm.vue new file mode 100644 index 0000000000..ed8ec0b426 --- /dev/null +++ b/packages/vue-to-dsl/test/full/input/appdemo01/src/views/createVm.vue @@ -0,0 +1,442 @@ + + + + diff --git a/packages/vue-to-dsl/test/full/input/appdemo01/vite.config.js b/packages/vue-to-dsl/test/full/input/appdemo01/vite.config.js new file mode 100644 index 0000000000..b611ebc4a3 --- /dev/null +++ b/packages/vue-to-dsl/test/full/input/appdemo01/vite.config.js @@ -0,0 +1,24 @@ +import { defineConfig } from 'vite' +import path from 'path' +import vue from '@vitejs/plugin-vue' +import vueJsx from '@vitejs/plugin-vue-jsx' + +export default defineConfig({ + resolve: { + alias: { + '@': path.resolve(__dirname, 'src') + } + }, + plugins: [vue(), vueJsx()], + define: { + 'process.env': { ...process.env } + }, + build: { + minify: true, + commonjsOptions: { + transformMixedEsModules: true + }, + cssCodeSplit: false + }, + base: './' +}) diff --git a/packages/vue-to-dsl/test/sfc/converter.test.js b/packages/vue-to-dsl/test/sfc/converter.test.js new file mode 100644 index 0000000000..b7a8cb4a4b --- /dev/null +++ b/packages/vue-to-dsl/test/sfc/converter.test.js @@ -0,0 +1,123 @@ +import { describe, it, expect } from 'vitest' +import { VueToDslConverter } from '../../src/converter' + +describe('VueToDslConverter', () => { + const converter = new VueToDslConverter({ computed_flag: true }) + + it('should convert simple Vue SFC to DSL', async () => { + const vueCode = ` + + + + + + ` + + const result = await converter.convertFromString(vueCode) + + expect(result.errors).toHaveLength(0) + expect(result.schema).toBeDefined() + expect(result.schema.componentName).toBe('Page') + expect(result.schema.state).toBeDefined() + expect(result.schema.methods).toBeDefined() + expect(result.schema.css).toBeDefined() + expect(result.schema.children).toBeDefined() + }) + + it('should handle Vue Options API', async () => { + const vueCode = ` + + + + ` + + const result = await converter.convertFromString(vueCode) + + expect(result.errors).toHaveLength(0) + expect(result.schema).toBeDefined() + expect(result.schema.state).toBeDefined() + expect(result.schema.methods).toBeDefined() + expect(result.schema.lifeCycles).toBeDefined() + }) + + it('should parse + ` + + const result = await converter.convertFromString(vueCode) + + expect(result.errors).toHaveLength(0) + expect(result.schema.state.count).toBeDefined() + // lifecycle hook stored with key 'onMounted' + expect(result.schema.lifeCycles.onMounted).toBeDefined() + expect(result.schema.methods.inc).toBeDefined() + }) + + it('should omit computed by default when flag is false', async () => { + const vueCode = ` + + + + ` + + const defaultConverter = new VueToDslConverter() + const result = await defaultConverter.convertFromString(vueCode) + expect(result.errors).toHaveLength(0) + expect(result.schema.computed).toBeUndefined() + }) +}) diff --git a/packages/vue-to-dsl/test/testcases/001_simple/expected/schema.json b/packages/vue-to-dsl/test/testcases/001_simple/expected/schema.json new file mode 100644 index 0000000000..ffcad68012 --- /dev/null +++ b/packages/vue-to-dsl/test/testcases/001_simple/expected/schema.json @@ -0,0 +1,60 @@ +{ + "componentName": "Page", + "fileName": "component", + "meta": { + "name": "component" + }, + "state": { + "switchStatus": true + }, + "methods": { + "onClickNew": { + "type": "JSFunction", + "value": "function onClickNew(event) {\n this.state.switchStatus = !this.state.switchStatus\n}" + } + }, + "computed": {}, + "lifeCycles": { + "setup": { + "type": "JSFunction", + "value": "function setup({ props, state, watch, onMounted }) {\n console.log('setup')\n state.switchStatus = false\n}" + } + }, + "css": ".page-base-style {\n padding: 24px;\n background: #ffffff;\n}\n\n.block-base-style {\n margin: 16px;\n}\n\n.component-base-style {\n margin: 8px;\n}", + "children": [ + { + "componentName": "div", + "props": {}, + "children": [ + { + "componentName": "TinySwitch", + "props": { + "className": "component-base-style", + "modelValue": { + "type": "JSExpression", + "value": "this.state.switchStatus", + "model": true + } + }, + "children": [], + "id": "fv6wuu63" + }, + { + "componentName": "TinyButton", + "props": { + "text": "按钮文案", + "className": "component-base-style", + "onClick": { + "type": "JSExpression", + "value": "this.onClickNew" + } + }, + "children": [], + "id": "88gmxjxz" + } + ], + "id": "ngzpirht" + } + ], + "id": "g6s705pv" +} diff --git a/packages/vue-to-dsl/test/testcases/001_simple/input/component.vue b/packages/vue-to-dsl/test/testcases/001_simple/input/component.vue new file mode 100644 index 0000000000..c930701a99 --- /dev/null +++ b/packages/vue-to-dsl/test/testcases/001_simple/input/component.vue @@ -0,0 +1,49 @@ + + + + diff --git a/packages/vue-to-dsl/test/testcases/002_createVM/expected/schema.json b/packages/vue-to-dsl/test/testcases/002_createVM/expected/schema.json new file mode 100644 index 0000000000..b9d2c6319a --- /dev/null +++ b/packages/vue-to-dsl/test/testcases/002_createVM/expected/schema.json @@ -0,0 +1,1125 @@ +{ + "componentName": "Page", + "fileName": "component", + "meta": { + "name": "component" + }, + "state": { + "dataDisk": [1, 2, 3] + }, + "methods": {}, + "computed": {}, + "lifeCycles": {}, + "css": "body {\n background-color: #eef0f5;\n margin-bottom: 80px;\n}", + "children": [ + { + "componentName": "div", + "props": {}, + "children": [ + { + "componentName": "div", + "props": { + "style": "padding-bottom: 10px; padding-top: 10px" + }, + "children": [ + { + "componentName": "TinyTimeLine", + "props": { + "active": "2", + "style": "border-radius: 0px", + "horizontal": true, + "data": [ + { + "name": "基础配置" + }, + { + "name": "网络配置" + }, + { + "name": "高级配置" + }, + { + "name": "确认配置" + } + ] + }, + "children": [], + "id": "f6o3ajqj" + } + ], + "id": "md6cwygl" + }, + { + "componentName": "div", + "props": { + "style": "\n border-width: 1px;\n border-style: solid;\n border-radius: 4px;\n border-color: #fff;\n padding-top: 10px;\n padding-bottom: 10px;\n padding-left: 10px;\n padding-right: 10px;\n box-shadow: rgba(0, 0, 0, 0.1) 0px 1px 3px 0px;\n background-color: #fff;\n margin-bottom: 10px;\n " + }, + "children": [ + { + "componentName": "TinyForm", + "props": { + "labelWidth": "80px", + "labelPosition": "top", + "label-position": "left ", + "label-width": "150px", + "style": "border-radius: 0px", + "inline": false + }, + "children": [ + { + "componentName": "TinyFormItem", + "props": { + "label": "计费模式" + }, + "children": [ + { + "componentName": "TinyButtonGroup", + "props": { + "modelValue": "1", + "data": [ + { + "text": "包年/包月", + "value": "1" + }, + { + "text": "按需计费", + "value": "2" + } + ] + }, + "children": [], + "id": "ubwd8lfk" + } + ], + "id": "676a0faj" + }, + { + "componentName": "TinyFormItem", + "props": { + "label": "区域" + }, + "children": [ + { + "componentName": "TinyButtonGroup", + "props": { + "modelValue": "1", + "style": "border-radius: 0px; margin-right: 10px", + "data": [ + { + "text": "乌兰察布二零一", + "value": "1" + } + ] + }, + "children": [], + "id": "s13qy75a" + }, + { + "componentName": "span", + "props": { + "style": "background-color: [object Event]; color: #8a8e99; font-size: 12px" + }, + "children": [ + { + "componentName": "Text", + "props": { + "text": "温馨提示:页面左上角切换区域" + }, + "id": "43spgaa5" + } + ], + "id": "sfcukuue" + }, + { + "componentName": "span", + "props": { + "style": "display: block; color: #8a8e99; border-radius: 0px; font-size: 12px" + }, + "children": [ + { + "componentName": "Text", + "props": { + "text": "不同区域的云服务产品之间内网互不相通;请就近选择靠近您业务的区域,可减少网络时延,提高访问速度" + }, + "id": "bkvslsjy" + } + ], + "id": "dsregz0u" + } + ], + "id": "i6ol9j9w" + }, + { + "componentName": "TinyFormItem", + "props": { + "label": "可用区", + "style": "border-radius: 0px" + }, + "children": [ + { + "componentName": "TinyButtonGroup", + "props": { + "modelValue": "1", + "data": [ + { + "text": "可用区1", + "value": "1" + }, + { + "text": "可用区2", + "value": "2" + }, + { + "text": "可用区3", + "value": "3" + } + ] + }, + "children": [], + "id": "vwnsns4b" + } + ], + "id": "l3xly2oq" + } + ], + "id": "4ekrjalz" + } + ], + "id": "rvsu4gu6" + }, + { + "componentName": "div", + "props": { + "style": "\n border-width: 1px;\n border-style: solid;\n border-radius: 4px;\n border-color: #fff;\n padding-top: 10px;\n padding-bottom: 10px;\n padding-left: 10px;\n padding-right: 10px;\n box-shadow: rgba(0, 0, 0, 0.1) 0px 1px 3px 0px;\n background-color: #fff;\n margin-bottom: 10px;\n " + }, + "children": [ + { + "componentName": "TinyForm", + "props": { + "labelWidth": "80px", + "labelPosition": "top", + "label-position": "left ", + "label-width": "150px", + "style": "border-radius: 0px", + "inline": false + }, + "children": [ + { + "componentName": "TinyFormItem", + "props": { + "label": "CPU架构" + }, + "children": [ + { + "componentName": "TinyButtonGroup", + "props": { + "modelValue": "1", + "data": [ + { + "text": "x86计算", + "value": "1" + }, + { + "text": "鲲鹏计算", + "value": "2" + } + ] + }, + "children": [], + "id": "sa4dv21x" + } + ], + "id": "kgjdu3xq" + }, + { + "componentName": "TinyFormItem", + "props": { + "label": "区域" + }, + "children": [ + { + "componentName": "div", + "props": { + "style": "display: flex; justify-content: flex-start; align-items: center" + }, + "children": [ + { + "componentName": "div", + "props": { + "style": "display: flex; align-items: center; margin-right: 10px" + }, + "children": [ + { + "componentName": "span", + "props": { + "style": "width: 80px" + }, + "children": [ + { + "componentName": "Text", + "props": { + "text": "vCPUs" + }, + "id": "3qi9sj2n" + } + ], + "id": "pjytdebw" + }, + { + "componentName": "TinySelect", + "props": { + "modelValue": "", + "placeholder": "请选择", + "options": [ + { + "value": "1", + "label": "黄金糕" + }, + { + "value": "2", + "label": "双皮奶" + } + ] + }, + "children": [], + "id": "y0t8wovv" + } + ], + "id": "fx3sn05n" + }, + { + "componentName": "div", + "props": { + "style": "display: flex; align-items: center; margin-right: 10px" + }, + "children": [ + { + "componentName": "span", + "props": { + "style": "width: 80px; border-radius: 0px" + }, + "children": [ + { + "componentName": "Text", + "props": { + "text": "内存" + }, + "id": "vp34oq3k" + } + ], + "id": "0ft31pas" + }, + { + "componentName": "TinySelect", + "props": { + "modelValue": "", + "placeholder": "请选择", + "options": [ + { + "value": "1", + "label": "黄金糕" + }, + { + "value": "2", + "label": "双皮奶" + } + ] + }, + "children": [], + "id": "zt8zcs8t" + } + ], + "id": "95z4v9c1" + }, + { + "componentName": "div", + "props": { + "style": "display: flex; align-items: center" + }, + "children": [ + { + "componentName": "span", + "props": { + "style": "width: 80px" + }, + "children": [ + { + "componentName": "Text", + "props": { + "text": "规格名称" + }, + "id": "hib8vlsc" + } + ], + "id": "q1qpyi0h" + }, + { + "componentName": "TinySearch", + "props": { + "modelValue": "", + "placeholder": "输入关键词" + }, + "children": [], + "id": "krtylew9" + } + ], + "id": "fapyalc6" + } + ], + "id": "vs6rq4a7" + }, + { + "componentName": "div", + "props": { + "style": "border-radius: 0px" + }, + "children": [ + { + "componentName": "TinyButtonGroup", + "props": { + "modelValue": "1", + "style": "border-radius: 0px; margin-top: 12px", + "data": [ + { + "text": "通用计算型", + "value": "1" + }, + { + "text": "通用计算增强型", + "value": "2" + }, + { + "text": "内存优化型", + "value": "3" + }, + { + "text": "内存优化型", + "value": "4" + }, + { + "text": "磁盘增强型", + "value": "5" + }, + { + "text": "超高I/O型", + "value": "6" + }, + { + "text": "GPU加速型", + "value": "7" + } + ] + }, + "children": [], + "id": "w2rvp1zj" + }, + { + "componentName": "TinyGrid", + "props": { + "style": "margin-top: 12px; border-radius: 0px", + "auto-resize": true, + "editConfig": { + "trigger": "click", + "mode": "cell", + "showStatus": true + }, + "columns": [ + { + "type": "radio", + "width": 60 + }, + { + "field": "employees", + "title": "规格名称" + }, + { + "field": "created_date", + "title": "vCPUs | 内存(GiB)", + "sortable": true + }, + { + "field": "city", + "title": "CPU", + "sortable": true + }, + { + "title": "基准 / 最大带宽\t", + "sortable": true + }, + { + "title": "内网收发包", + "sortable": true + } + ], + "data": [ + { + "id": "1", + "name": "GFD科技有限公司", + "city": "福州", + "employees": 800, + "created_date": "2014-04-30 00:56:00", + "boole": false + }, + { + "id": "2", + "name": "WWW科技有限公司", + "city": "深圳", + "employees": 300, + "created_date": "2016-07-08 12:36:22", + "boole": true + } + ] + }, + "children": [], + "id": "56qdrmjc" + }, + { + "componentName": "div", + "props": { + "style": "margin-top: 12px; border-radius: 0px" + }, + "children": [ + { + "componentName": "span", + "props": { + "style": "width: 150px; display: inline-block" + }, + "children": [ + { + "componentName": "Text", + "props": { + "text": "当前规格" + }, + "id": "j0lbbokj" + } + ], + "id": "819b4ldc" + }, + { + "componentName": "span", + "props": { + "style": "font-weight: 700" + }, + "children": [ + { + "componentName": "Text", + "props": { + "text": "通用计算型 | Si2.large.2 | 2vCPUs | 4 GiB" + }, + "id": "mhxn1jsh" + } + ], + "id": "nroj6wso" + } + ], + "id": "kf0lnpnv" + } + ], + "id": "tk67ku85" + } + ], + "id": "08j8y1ff" + } + ], + "id": "dqcoos16" + } + ], + "id": "wgnx6mrw" + }, + { + "componentName": "div", + "props": { + "style": "\n border-width: 1px;\n border-style: solid;\n border-radius: 4px;\n border-color: #fff;\n padding-top: 10px;\n padding-bottom: 10px;\n padding-left: 10px;\n padding-right: 10px;\n box-shadow: rgba(0, 0, 0, 0.1) 0px 1px 3px 0px;\n background-color: #fff;\n margin-bottom: 10px;\n " + }, + "children": [ + { + "componentName": "TinyForm", + "props": { + "labelWidth": "80px", + "labelPosition": "top", + "label-position": "left ", + "label-width": "150px", + "style": "border-radius: 0px", + "inline": false + }, + "children": [ + { + "componentName": "TinyFormItem", + "props": { + "label": "镜像", + "style": "border-radius: 0px" + }, + "children": [ + { + "componentName": "TinyButtonGroup", + "props": { + "modelValue": "1", + "data": [ + { + "text": "公共镜像", + "value": "1" + }, + { + "text": "私有镜像", + "value": "2" + }, + { + "text": "共享镜像", + "value": "3" + } + ] + }, + "children": [], + "id": "3pmrh1zz" + }, + { + "componentName": "div", + "props": { + "style": "display: flex; margin-top: 12px; border-radius: 0px" + }, + "children": [ + { + "componentName": "TinySelect", + "props": { + "modelValue": "", + "placeholder": "请选择", + "style": "width: 170px; margin-right: 10px", + "options": [ + { + "value": "1", + "label": "黄金糕" + }, + { + "value": "2", + "label": "双皮奶" + } + ] + }, + "children": [], + "id": "4s9tm0oo" + }, + { + "componentName": "TinySelect", + "props": { + "modelValue": "", + "placeholder": "请选择", + "style": "width: 340px", + "options": [ + { + "value": "1", + "label": "黄金糕" + }, + { + "value": "2", + "label": "双皮奶" + } + ] + }, + "children": [], + "id": "i0ou3048" + } + ], + "id": "rawlzvaa" + }, + { + "componentName": "div", + "props": { + "style": "margin-top: 12px" + }, + "children": [ + { + "componentName": "span", + "props": { + "style": "color: #e37d29" + }, + "children": [ + { + "componentName": "Text", + "props": { + "text": "请注意操作系统的语言类型。" + }, + "id": "vlnnyrxv" + } + ], + "id": "fkhps62d" + } + ], + "id": "laqstaoh" + } + ], + "id": "m8as5pxu" + } + ], + "id": "7pb83c31" + } + ], + "id": "3ng26b65" + }, + { + "componentName": "div", + "props": { + "style": "\n border-width: 1px;\n border-style: solid;\n border-radius: 4px;\n border-color: #fff;\n padding-top: 10px;\n padding-bottom: 10px;\n padding-left: 10px;\n padding-right: 10px;\n box-shadow: rgba(0, 0, 0, 0.1) 0px 1px 3px 0px;\n background-color: #fff;\n margin-bottom: 10px;\n " + }, + "children": [ + { + "componentName": "TinyForm", + "props": { + "labelWidth": "80px", + "labelPosition": "top", + "label-position": "left ", + "label-width": "150px", + "style": "border-radius: 0px", + "inline": false + }, + "children": [ + { + "componentName": "TinyFormItem", + "props": { + "label": "系统盘", + "style": "border-radius: 0px" + }, + "children": [ + { + "componentName": "div", + "props": { + "style": "display: flex" + }, + "children": [ + { + "componentName": "TinySelect", + "props": { + "modelValue": "", + "placeholder": "请选择", + "style": "width: 200px; margin-right: 10px", + "options": [ + { + "value": "1", + "label": "黄金糕" + }, + { + "value": "2", + "label": "双皮奶" + } + ] + }, + "children": [], + "id": "w3pe0p5l" + }, + { + "componentName": "TinyInput", + "props": { + "placeholder": "请输入", + "modelValue": "", + "style": "width: 120px; margin-right: 10px" + }, + "children": [], + "id": "hhs8g284" + }, + { + "componentName": "span", + "props": { + "style": "color: #575d6c; font-size: 12px" + }, + "children": [ + { + "componentName": "Text", + "props": { + "text": "GiB IOPS上限240,IOPS突发上限5,000" + }, + "id": "zj1dyvyc" + } + ], + "id": "b5accadg" + } + ], + "id": "ro8z737r" + } + ], + "id": "c6513skc" + } + ], + "id": "bqckjdbt" + }, + { + "componentName": "TinyForm", + "props": { + "labelWidth": "80px", + "labelPosition": "top", + "label-position": "left ", + "label-width": "150px", + "style": "border-radius: 0px", + "inline": false + }, + "children": [ + { + "componentName": "TinyFormItem", + "props": { + "label": "数据盘", + "style": "border-radius: 0px" + }, + "children": [ + { + "componentName": "div", + "props": { + "style": "margin-top: 12px; display: flex" + }, + "children": [ + { + "componentName": "Icon", + "props": { + "style": "margin-right: 10px; width: 16px; height: 16px", + "name": "IconPanelMini" + }, + "children": [], + "id": "in4uom22" + }, + { + "componentName": "TinySelect", + "props": { + "modelValue": "", + "placeholder": "请选择", + "style": "width: 200px; margin-right: 10px", + "options": [ + { + "value": "1", + "label": "黄金糕" + }, + { + "value": "2", + "label": "双皮奶" + } + ] + }, + "children": [], + "id": "f9b3adim" + }, + { + "componentName": "TinyInput", + "props": { + "placeholder": "请输入", + "modelValue": "", + "style": "width: 120px; margin-right: 10px" + }, + "children": [], + "id": "hkc6mfzd" + }, + { + "componentName": "span", + "props": { + "style": "color: #575d6c; font-size: 12px; margin-right: 10px" + }, + "children": [ + { + "componentName": "Text", + "props": { + "text": "GiB IOPS上限600,IOPS突发上限5,000" + }, + "id": "gu5suwyv" + } + ], + "id": "4f38cvox" + }, + { + "componentName": "TinyInput", + "props": { + "placeholder": "请输入", + "modelValue": "", + "style": "width: 120px" + }, + "children": [], + "id": "oj3nt5s1" + } + ], + "loop": { + "type": "JSExpression", + "value": "this.state.dataDisk" + }, + "id": "c74s7y9n" + }, + { + "componentName": "div", + "props": { + "style": "display: flex; margin-top: 12px; border-radius: 0px" + }, + "children": [ + { + "componentName": "Icon", + "props": { + "style": "width: 16px; height: 16px; margin-right: 10px", + "name": "IconPlus" + }, + "children": [], + "id": "ze75z2il" + }, + { + "componentName": "span", + "props": { + "style": "font-size: 12px; border-radius: 0px; margin-right: 10px" + }, + "children": [ + { + "componentName": "Text", + "props": { + "text": "增加一块数据盘" + }, + "id": "wmbogfnt" + } + ], + "id": "jrvc6fj3" + }, + { + "componentName": "span", + "props": { + "style": "color: #8a8e99; font-size: 12px" + }, + "children": [ + { + "componentName": "Text", + "props": { + "text": "您还可以挂载 21 块磁盘(云硬盘)" + }, + "id": "7w1z3fem" + } + ], + "id": "pczkwydn" + } + ], + "id": "dn6ab3hv" + } + ], + "id": "j260lifx" + } + ], + "id": "g46smnve" + } + ], + "id": "ccze0iwn" + }, + { + "componentName": "div", + "props": { + "style": "\n border-width: 1px;\n border-style: solid;\n border-color: #ffffff;\n padding-top: 10px;\n padding-left: 10px;\n padding-right: 10px;\n box-shadow: rgba(0, 0, 0, 0.1) 0px 1px 3px 0px;\n background-color: #fff;\n position: fixed;\n inset: auto 0% 0% 0%;\n height: 80px;\n line-height: 80px;\n border-radius: 0px;\n " + }, + "children": [ + { + "componentName": "TinyForm", + "props": { + "labelWidth": "80px", + "labelPosition": "top", + "label-position": "left ", + "label-width": "150px", + "style": "border-radius: 0px", + "inline": false + }, + "children": [], + "id": "k4z8lqi7" + }, + { + "componentName": "TinyRow", + "props": { + "style": "border-radius: 0px; height: 100%" + }, + "children": [ + { + "componentName": "TinyCol", + "props": { + "span": "8" + }, + "children": [ + { + "componentName": "TinyRow", + "props": { + "style": "border-radius: 0px" + }, + "children": [ + { + "componentName": "TinyCol", + "props": { + "span": "5", + "style": "display: flex" + }, + "children": [ + { + "componentName": "span", + "props": { + "style": "margin-right: 10px" + }, + "children": [ + { + "componentName": "Text", + "props": { + "text": "购买量" + }, + "id": "o1jcb13q" + } + ], + "id": "qnptw0yo" + }, + { + "componentName": "TinyInput", + "props": { + "placeholder": "请输入", + "modelValue": "", + "style": "width: 120px; margin-right: 10px" + }, + "children": [], + "id": "jwlwlvz0" + }, + { + "componentName": "span", + "props": {}, + "children": [ + { + "componentName": "Text", + "props": { + "text": "台" + }, + "id": "jmfizbf2" + } + ], + "id": "uxtzimy2" + } + ], + "id": "pm92xrj9" + }, + { + "componentName": "TinyCol", + "props": { + "span": "7" + }, + "children": [ + { + "componentName": "div", + "props": {}, + "children": [ + { + "componentName": "span", + "props": { + "style": "font-size: 12px" + }, + "children": [ + { + "componentName": "Text", + "props": { + "text": "配置费用" + }, + "id": "xxiefanz" + } + ], + "id": "ae4vk0vk" + }, + { + "componentName": "span", + "props": { + "style": "padding-left: 10px; padding-right: 10px; color: #de504e" + }, + "children": [ + { + "componentName": "Text", + "props": { + "text": "¥1.5776" + }, + "id": "i40ub6sn" + } + ], + "id": "cb02qhdh" + }, + { + "componentName": "span", + "props": { + "style": "font-size: 12px" + }, + "children": [ + { + "componentName": "Text", + "props": { + "text": "/小时" + }, + "id": "adkjij9o" + } + ], + "id": "k2x66nf0" + } + ], + "id": "n1d16xku" + }, + { + "componentName": "div", + "props": {}, + "children": [ + { + "componentName": "span", + "props": { + "style": "font-size: 12px; border-radius: 0px" + }, + "children": [ + { + "componentName": "Text", + "props": { + "text": "参考价格,具体扣费请以账单为准。" + }, + "id": "vmd1mfi5" + } + ], + "id": "dpi9os07" + }, + { + "componentName": "span", + "props": { + "style": "font-size: 12px; color: #344899" + }, + "children": [ + { + "componentName": "Text", + "props": { + "text": "了解计费详情" + }, + "id": "eq0byqdt" + } + ], + "id": "n8tt8sko" + } + ], + "id": "q9wmr8ma" + } + ], + "id": "cwequlwq" + } + ], + "id": "8jwt4esp" + } + ], + "id": "sy3gd183" + }, + { + "componentName": "TinyCol", + "props": { + "span": "4", + "style": "\n display: flex;\n flex-direction: row-reverse;\n border-radius: 0px;\n height: 100%;\n justify-content: flex-start;\n align-items: center;\n " + }, + "children": [ + { + "componentName": "TinyButton", + "props": { + "text": "下一步: 网络配置", + "type": "danger", + "style": "max-width: unset" + }, + "children": [], + "id": "skzmpiap" + } + ], + "id": "n9wdnvab" + } + ], + "id": "rjuainel" + } + ], + "id": "veqg3b07" + } + ], + "id": "z9geov13" + } + ], + "id": "ozevdge6" +} diff --git a/packages/vue-to-dsl/test/testcases/002_createVM/input/component.vue b/packages/vue-to-dsl/test/testcases/002_createVM/input/component.vue new file mode 100644 index 0000000000..57881bbb7a --- /dev/null +++ b/packages/vue-to-dsl/test/testcases/002_createVM/input/component.vue @@ -0,0 +1,411 @@ + + + + diff --git a/packages/vue-to-dsl/test/testcases/003_login/expected/schema.json b/packages/vue-to-dsl/test/testcases/003_login/expected/schema.json new file mode 100644 index 0000000000..da4a931af2 --- /dev/null +++ b/packages/vue-to-dsl/test/testcases/003_login/expected/schema.json @@ -0,0 +1,254 @@ +{ + "componentName": "Page", + "fileName": "component", + "meta": { + "name": "component" + }, + "state": { + "form": { + "username": "", + "password": "" + } + }, + "methods": { + "onSubmit": { + "type": "JSFunction", + "value": "function onSubmit() {\n if (!state.form.username || !state.form.password) return\n console.log('login', state.form)\n}" + }, + "onReset": { + "type": "JSFunction", + "value": "function onReset() {\n state.form.username = ''\n state.form.password = ''\n}" + } + }, + "computed": {}, + "lifeCycles": {}, + "css": ".login-page {\n min-height: 100vh;\n display: flex;\n align-items: center;\n justify-content: center;\n padding: 24px;\n background: linear-gradient(135deg, #f5f7fa 0%, #e9eff7 100%);\n}\n\n.login-card {\n width: 420px;\n background: #fff;\n border: 1px solid #eef0f5;\n border-radius: 12px;\n box-shadow: 0 8px 24px rgba(0, 0, 0, 0.06);\n padding: 24px 24px 16px;\n}\n\n.login-header {\n text-align: center;\n margin-bottom: 12px;\n}\n\n.login-title {\n margin: 0;\n font-size: 22px;\n color: #1f2329;\n}\n\n.login-subtitle {\n margin: 6px 0 0;\n color: #6b7280;\n font-size: 12px;\n}\n\n.login-extra {\n display: flex;\n justify-content: space-between;\n font-size: 12px;\n}\n\n.link {\n color: #344899;\n text-decoration: none;\n}\n\n.link:hover {\n text-decoration: underline;\n}", + "children": [ + { + "componentName": "div", + "props": { + "className": "login-page" + }, + "children": [ + { + "componentName": "div", + "props": { + "className": "login-card" + }, + "children": [ + { + "componentName": "div", + "props": { + "className": "login-header" + }, + "children": [ + { + "componentName": "h2", + "props": { + "className": "login-title" + }, + "children": [ + { + "componentName": "Text", + "props": { + "text": "欢迎登录" + }, + "id": "ofvczp1s" + } + ], + "id": "x8ef07lh" + } + ], + "id": "1jfod2b7" + }, + { + "componentName": "TinyForm", + "props": { + "label-width": "80px", + "label-position": "left", + "inline": false + }, + "children": [ + { + "componentName": "TinyFormItem", + "props": { + "label": "用户名" + }, + "children": [ + { + "componentName": "TinyInput", + "props": { + "placeholder": "请输入用户名", + "modelValue": { + "type": "JSExpression", + "value": "this.state.form.username", + "model": true + } + }, + "children": [], + "id": "1ygfx967" + } + ], + "id": "9c9vcnnl" + }, + { + "componentName": "TinyFormItem", + "props": { + "label": "密码" + }, + "children": [ + { + "componentName": "TinyInput", + "props": { + "type": "password", + "placeholder": "请输入密码", + "modelValue": { + "type": "JSExpression", + "value": "this.state.form.password", + "model": true + } + }, + "children": [], + "id": "n7apt8qz" + } + ], + "id": "ic28zqv9" + }, + { + "componentName": "TinyFormItem", + "props": {}, + "children": [ + { + "componentName": "TinyRow", + "props": { + "gutter": 8 + }, + "children": [ + { + "componentName": "TinyCol", + "props": { + "span": 12 + }, + "children": [ + { + "componentName": "TinyButton", + "props": { + "type": "primary", + "style": "width: 100%", + "onClick": { + "type": "JSExpression", + "value": "this.onSubmit" + } + }, + "children": [ + { + "componentName": "Text", + "props": { + "text": "登录" + }, + "id": "2nrmrn45" + } + ], + "id": "19hic5dx" + } + ], + "id": "y407fl72" + }, + { + "componentName": "TinyCol", + "props": { + "span": 12 + }, + "children": [ + { + "componentName": "TinyButton", + "props": { + "type": "default", + "style": "width: 100%", + "onClick": { + "type": "JSExpression", + "value": "this.onReset" + } + }, + "children": [ + { + "componentName": "Text", + "props": { + "text": "重置" + }, + "id": "whtbv216" + } + ], + "id": "vbz134gr" + } + ], + "id": "pttzhyn8" + } + ], + "id": "rk9r36ku" + } + ], + "id": "61dtkdhp" + }, + { + "componentName": "TinyFormItem", + "props": {}, + "children": [ + { + "componentName": "div", + "props": { + "className": "login-extra" + }, + "children": [ + { + "componentName": "a", + "props": { + "href": "#", + "className": "link" + }, + "children": [ + { + "componentName": "Text", + "props": { + "text": "忘记密码?" + }, + "id": "3sho8h0e" + } + ], + "id": "z3ct2qcg" + }, + { + "componentName": "a", + "props": { + "href": "#", + "className": "link" + }, + "children": [ + { + "componentName": "Text", + "props": { + "text": "注册账号" + }, + "id": "sl7n8oa0" + } + ], + "id": "vcbhy095" + } + ], + "id": "hs39mc41" + } + ], + "id": "fwkcsif8" + } + ], + "id": "vznait0f" + } + ], + "id": "2ia7lcq3" + } + ], + "id": "p79r4yv8" + } + ], + "id": "qxpzn62z" +} diff --git a/packages/vue-to-dsl/test/testcases/003_login/input/component.vue b/packages/vue-to-dsl/test/testcases/003_login/input/component.vue new file mode 100644 index 0000000000..34020e34ea --- /dev/null +++ b/packages/vue-to-dsl/test/testcases/003_login/input/component.vue @@ -0,0 +1,106 @@ + + + + + diff --git a/packages/vue-to-dsl/test/testcases/004_dashboard/expected/schema.json b/packages/vue-to-dsl/test/testcases/004_dashboard/expected/schema.json new file mode 100644 index 0000000000..13938d5ab6 --- /dev/null +++ b/packages/vue-to-dsl/test/testcases/004_dashboard/expected/schema.json @@ -0,0 +1,408 @@ +{ + "componentName": "Page", + "fileName": "component", + "meta": { + "name": "component" + }, + "state": { + "activeStep": 1, + "filters": { + "keyword": "", + "module": "" + }, + "category": "all", + "steps": [ + { + "name": "准备" + }, + { + "name": "处理" + }, + { + "name": "完成" + } + ], + "columns": [ + { + "field": "name", + "title": "名称" + }, + { + "field": "path", + "title": "路径" + } + ], + "quickLinks": [ + { + "id": 1, + "name": "用户管理", + "path": "/users" + }, + { + "id": 2, + "name": "订单管理", + "path": "/orders" + } + ], + "modules": [ + { + "value": "user", + "label": "用户" + }, + { + "value": "order", + "label": "订单" + }, + { + "value": "report", + "label": "报表" + } + ], + "categories": [ + { + "text": "全部", + "value": "all" + }, + { + "text": "常用", + "value": "fav" + }, + { + "text": "最近", + "value": "recent" + } + ], + "todoCols": [ + { + "field": "title", + "title": "标题" + }, + { + "field": "deadline", + "title": "截止时间" + } + ], + "todos": [ + { + "id": 1, + "title": "修复登录问题", + "deadline": "2025-09-30" + }, + { + "id": 2, + "title": "升级依赖", + "deadline": "2025-10-10" + } + ] + }, + "methods": { + "onSearch": { + "type": "JSFunction", + "value": "function onSearch() {\n console.log('search with', state.filters, state.category)\n}" + }, + "onReset": { + "type": "JSFunction", + "value": "function onReset() {\n state.filters.keyword = ''\n state.filters.module = ''\n state.category = 'all'\n}" + }, + "go": { + "type": "JSFunction", + "value": "function go(item) {\n console.log('go to', item.path)\n}" + } + }, + "computed": {}, + "lifeCycles": {}, + "css": ".dashboard-page {\n padding: 16px;\n}\n\n.panel {\n padding: 12px;\n border: 1px solid #eee;\n border-radius: 4px;\n background: #fff;\n}\n\n.panel-title {\n margin: 0 0 8px;\n font-size: 16px;\n font-weight: 600;\n}\n\n.quick-links {\n display: grid;\n grid-template-columns: repeat(4, minmax(0, 1fr));\n gap: 12px;\n margin-bottom: 12px;\n}\n\n.quick-link-tile {\n height: 80px;\n border: 1px solid #e5e7eb;\n border-radius: 8px;\n background: #fafafa;\n display: flex;\n align-items: center;\n justify-content: center;\n cursor: pointer;\n transition: all 0.2s ease;\n}\n\n.quick-link-tile:hover {\n background: #f0f6ff;\n border-color: #cfe0ff;\n box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);\n}\n\n.quick-link-text {\n color: #1f2329;\n font-size: 14px;\n font-weight: 500;\n}", + "children": [ + { + "componentName": "div", + "props": { + "className": "dashboard-page" + }, + "children": [ + { + "componentName": "TinyTimeLine", + "props": { + "style": "margin-bottom: 12px", + "horizontal": true, + "active": { + "type": "JSExpression", + "value": "this.state.activeStep" + }, + "data": { + "type": "JSExpression", + "value": "this.state.steps" + } + }, + "children": [], + "id": "f59raon9" + }, + { + "componentName": "div", + "props": { + "className": "panel", + "style": "margin-bottom: 12px" + }, + "children": [ + { + "componentName": "h3", + "props": { + "className": "panel-title" + }, + "children": [ + { + "componentName": "Text", + "props": { + "text": "筛选" + }, + "id": "kkgfrui1" + } + ], + "id": "dwwurpcj" + }, + { + "componentName": "TinyForm", + "props": { + "label-width": "80px", + "label-position": "left", + "inline": true + }, + "children": [ + { + "componentName": "TinyFormItem", + "props": { + "label": "关键词" + }, + "children": [ + { + "componentName": "TinySearch", + "props": { + "placeholder": "请输入关键词", + "modelValue": { + "type": "JSExpression", + "value": "this.state.filters.keyword", + "model": true + } + }, + "children": [], + "id": "5m0pkah7" + } + ], + "id": "y3goleeb" + }, + { + "componentName": "TinyFormItem", + "props": { + "label": "模块" + }, + "children": [ + { + "componentName": "TinySelect", + "props": { + "placeholder": "请选择模块", + "modelValue": { + "type": "JSExpression", + "value": "this.state.filters.module", + "model": true + }, + "options": { + "type": "JSExpression", + "value": "this.state.modules" + } + }, + "children": [], + "id": "g7sbqnft" + } + ], + "id": "caz07g6b" + }, + { + "componentName": "TinyFormItem", + "props": { + "label": "分类" + }, + "children": [ + { + "componentName": "TinyButtonGroup", + "props": { + "modelValue": { + "type": "JSExpression", + "value": "this.state.category", + "model": true + }, + "data": { + "type": "JSExpression", + "value": "this.state.categories" + } + }, + "children": [], + "id": "lwb0qf2l" + } + ], + "id": "ta8d6xus" + }, + { + "componentName": "TinyFormItem", + "props": {}, + "children": [ + { + "componentName": "TinyButton", + "props": { + "type": "primary", + "onClick": { + "type": "JSExpression", + "value": "this.onSearch" + } + }, + "children": [ + { + "componentName": "Text", + "props": { + "text": "搜索" + }, + "id": "fiygyf9b" + } + ], + "id": "acrjkrp2" + }, + { + "componentName": "TinyButton", + "props": { + "style": "margin-left: 8px", + "onClick": { + "type": "JSExpression", + "value": "this.onReset" + } + }, + "children": [ + { + "componentName": "Text", + "props": { + "text": "重置" + }, + "id": "w2optwrv" + } + ], + "id": "hkb5xq8q" + } + ], + "id": "no24uuys" + } + ], + "id": "i87ca3b3" + } + ], + "id": "tjkp78uw" + }, + { + "componentName": "TinyRow", + "props": {}, + "children": [ + { + "componentName": "TinyCol", + "props": { + "span": 12 + }, + "children": [ + { + "componentName": "div", + "props": { + "className": "panel" + }, + "children": [ + { + "componentName": "h3", + "props": { + "className": "panel-title" + }, + "children": [ + { + "componentName": "Text", + "props": { + "text": "快速入口" + }, + "id": "5135tk3c" + } + ], + "id": "rjy5v72m" + }, + { + "componentName": "TinyGrid", + "props": { + "columns": { + "type": "JSExpression", + "value": "this.state.columns" + }, + "data": { + "type": "JSExpression", + "value": "this.state.quickLinks" + }, + "auto-resize": true + }, + "children": [], + "id": "xciifp02" + } + ], + "id": "h27vtj8q" + } + ], + "id": "7565lb42" + }, + { + "componentName": "TinyCol", + "props": { + "span": 12 + }, + "children": [ + { + "componentName": "div", + "props": { + "className": "panel" + }, + "children": [ + { + "componentName": "h3", + "props": { + "className": "panel-title" + }, + "children": [ + { + "componentName": "Text", + "props": { + "text": "待办事项" + }, + "id": "2vpoheon" + } + ], + "id": "45q9o8z0" + }, + { + "componentName": "TinyGrid", + "props": { + "columns": { + "type": "JSExpression", + "value": "this.state.todoCols" + }, + "data": { + "type": "JSExpression", + "value": "this.state.todos" + }, + "auto-resize": true + }, + "children": [], + "id": "6o2fqs22" + } + ], + "id": "e1u7pepk" + } + ], + "id": "g32ur0yx" + } + ], + "id": "phzx3kdm" + } + ], + "id": "1z55g7br" + } + ], + "id": "6kkb8knu" +} diff --git a/packages/vue-to-dsl/test/testcases/004_dashboard/input/component.vue b/packages/vue-to-dsl/test/testcases/004_dashboard/input/component.vue new file mode 100644 index 0000000000..e5dd7a0507 --- /dev/null +++ b/packages/vue-to-dsl/test/testcases/004_dashboard/input/component.vue @@ -0,0 +1,141 @@ + + + + + diff --git a/packages/vue-to-dsl/test/testcases/005_survey/expected/schema.json b/packages/vue-to-dsl/test/testcases/005_survey/expected/schema.json new file mode 100644 index 0000000000..0dc5e78ee0 --- /dev/null +++ b/packages/vue-to-dsl/test/testcases/005_survey/expected/schema.json @@ -0,0 +1,223 @@ +{ + "componentName": "Page", + "fileName": "component", + "meta": { + "name": "component" + }, + "state": { + "name": "", + "satisfaction": 3, + "feedback": "", + "recommend": true + }, + "methods": { + "submitSurvey": { + "type": "JSFunction", + "value": "function submitSurvey() {\n console.log('提交满意度调查:', {\n name: this.state.name,\n satisfaction: this.state.satisfaction,\n feedback: this.state.feedback,\n recommend: this.state.recommend\n })\n // 这里可以添加提交到服务器的逻辑\n alert('感谢您的反馈!')\n}" + } + }, + "computed": {}, + "lifeCycles": {}, + "css": ".page-base-style {\n padding: 24px;\n background: #ffffff;\n}\n\n.block-base-style {\n margin: 16px;\n}\n\n.component-base-style {\n margin: 8px;\n}", + "children": [ + { + "componentName": "div", + "props": { + "className": "page-base-style" + }, + "children": [ + { + "componentName": "TinyForm", + "props": { + "labelWidth": "100px", + "className": "component-base-style" + }, + "children": [ + { + "componentName": "TinyFormItem", + "props": { + "label": "姓名" + }, + "children": [ + { + "componentName": "TinyInput", + "props": { + "placeholder": "请输入姓名", + "className": "component-base-style", + "modelValue": { + "type": "JSExpression", + "value": "this.state.name", + "model": true + } + }, + "children": [], + "id": "btf77s93" + } + ], + "id": "m0nep3a0" + }, + { + "componentName": "TinyFormItem", + "props": { + "label": "整体满意度" + }, + "children": [ + { + "componentName": "div", + "props": { + "style": "display: flex; flex-wrap: wrap; gap: 12px" + }, + "children": [ + { + "componentName": "TinyRadio", + "props": { + "text": "非常满意", + "label": 5, + "modelValue": { + "type": "JSExpression", + "value": "this.state.satisfaction", + "model": true + } + }, + "children": [], + "id": "r0o027ty" + }, + { + "componentName": "TinyRadio", + "props": { + "text": "满意", + "label": 4, + "modelValue": { + "type": "JSExpression", + "value": "this.state.satisfaction", + "model": true + } + }, + "children": [], + "id": "moq6l88f" + }, + { + "componentName": "TinyRadio", + "props": { + "text": "一般", + "label": 3, + "modelValue": { + "type": "JSExpression", + "value": "this.state.satisfaction", + "model": true + } + }, + "children": [], + "id": "hwydklui" + }, + { + "componentName": "TinyRadio", + "props": { + "text": "不满意", + "label": 2, + "modelValue": { + "type": "JSExpression", + "value": "this.state.satisfaction", + "model": true + } + }, + "children": [], + "id": "hptyv62f" + }, + { + "componentName": "TinyRadio", + "props": { + "text": "非常不满意", + "label": 1, + "modelValue": { + "type": "JSExpression", + "value": "this.state.satisfaction", + "model": true + } + }, + "children": [], + "id": "mr5ipgmo" + } + ], + "id": "nfer8dbn" + } + ], + "id": "1dpt0uh8" + }, + { + "componentName": "TinyFormItem", + "props": { + "label": "具体意见" + }, + "children": [ + { + "componentName": "TinyInput", + "props": { + "type": "textarea", + "placeholder": "请提出宝贵意见", + "className": "component-base-style", + "modelValue": { + "type": "JSExpression", + "value": "this.state.feedback", + "model": true + } + }, + "children": [], + "id": "c5qczlnv" + } + ], + "id": "94ebcml9" + }, + { + "componentName": "TinyFormItem", + "props": { + "label": "是否愿意推荐" + }, + "children": [ + { + "componentName": "TinySwitch", + "props": { + "modelValue": { + "type": "JSExpression", + "value": "this.state.recommend", + "model": true + } + }, + "children": [], + "id": "m2pgtpsd" + } + ], + "id": "6p4xx6mb" + }, + { + "componentName": "TinyFormItem", + "props": { + "label": "" + }, + "children": [ + { + "componentName": "TinyButton", + "props": { + "text": "提交", + "type": "primary", + "className": "component-base-style", + "onClick": { + "type": "JSExpression", + "value": "this.submitSurvey" + } + }, + "children": [], + "id": "q1isiake" + } + ], + "id": "mx5qz17f" + } + ], + "id": "3ny8gp70" + } + ], + "id": "t58jntyo" + } + ], + "id": "88gufz3b" +} diff --git a/packages/vue-to-dsl/test/testcases/005_survey/input/component.vue b/packages/vue-to-dsl/test/testcases/005_survey/input/component.vue new file mode 100644 index 0000000000..fb3155e636 --- /dev/null +++ b/packages/vue-to-dsl/test/testcases/005_survey/input/component.vue @@ -0,0 +1,85 @@ + + + + diff --git a/packages/vue-to-dsl/test/testcases/006_lifecycle/expected/schema.json b/packages/vue-to-dsl/test/testcases/006_lifecycle/expected/schema.json new file mode 100644 index 0000000000..70e7caa872 --- /dev/null +++ b/packages/vue-to-dsl/test/testcases/006_lifecycle/expected/schema.json @@ -0,0 +1,64 @@ +{ + "componentName": "Page", + "fileName": "component", + "meta": { + "name": "component" + }, + "state": {}, + "methods": {}, + "computed": {}, + "lifeCycles": { + "onMounted": { + "type": "JSFunction", + "value": "function onMounted() {\n console.log('mounted')\n}" + }, + "onUpdated": { + "type": "JSFunction", + "value": "function onUpdated() {\n console.log('updated')\n}" + }, + "onUnmounted": { + "type": "JSFunction", + "value": "function onUnmounted() {\n console.log('unmounted')\n}" + }, + "onBeforeMount": { + "type": "JSFunction", + "value": "function onBeforeMount() {\n console.log('before mount')\n}" + }, + "onBeforeUpdate": { + "type": "JSFunction", + "value": "function onBeforeUpdate() {\n console.log('before update')\n}" + }, + "onBeforeUnmount": { + "type": "JSFunction", + "value": "function onBeforeUnmount() {\n console.log('before unmount')\n}" + }, + "onActivated": { + "type": "JSFunction", + "value": "function onActivated() {\n console.log('activated')\n}" + }, + "onDeactivated": { + "type": "JSFunction", + "value": "function onDeactivated() {\n console.log('deactivated')\n}" + } + }, + "css": ".lifecycle-container {\n color: #333;\n}", + "children": [ + { + "componentName": "div", + "props": { + "className": "lifecycle-container" + }, + "children": [ + { + "componentName": "Text", + "props": { + "text": "Lifecycle" + }, + "id": "lnvsla7q" + } + ], + "id": "rrfdkail" + } + ], + "id": "b30pdz9q" +} diff --git a/packages/vue-to-dsl/test/testcases/006_lifecycle/input/component.vue b/packages/vue-to-dsl/test/testcases/006_lifecycle/input/component.vue new file mode 100644 index 0000000000..befbe30bd5 --- /dev/null +++ b/packages/vue-to-dsl/test/testcases/006_lifecycle/input/component.vue @@ -0,0 +1,54 @@ + + + + + diff --git a/packages/vue-to-dsl/test/testcases/index.test.js b/packages/vue-to-dsl/test/testcases/index.test.js new file mode 100644 index 0000000000..d44d7369f3 --- /dev/null +++ b/packages/vue-to-dsl/test/testcases/index.test.js @@ -0,0 +1,98 @@ +import { describe, it, expect } from 'vitest' +import { VueToDslConverter } from '../../src/converter' +import fs from 'fs' +import path from 'path' + +describe('VueToDslConverter testcases', () => { + const baseDir = path.resolve(__dirname, '.') + const converter = new VueToDslConverter({ computed_flag: true }) + + const cases = fs.readdirSync(baseDir).filter((name) => /\d+_/.test(name)) + + cases.forEach((caseName) => { + const caseDir = path.join(baseDir, caseName) + const inputFile = path.join(caseDir, 'input', 'component.vue') + const expectFile = path.join(caseDir, 'expected', 'schema.json') + + it(`case ${caseName} should convert correctly`, async () => { + const result = await converter.convertFromFile(inputFile) + const expected = JSON.parse(fs.readFileSync(expectFile, 'utf-8')) + + // 保存到output目录 + const outputFile = path.join(caseDir, 'output', 'schema.json') + fs.mkdirSync(path.dirname(outputFile), { recursive: true }) + fs.writeFileSync(outputFile, JSON.stringify(result.schema, null, 2)) + + // helper: deep clean (remove dynamic keys like meta / id) + const deepClean = (val) => { + if (Array.isArray(val)) { + return val.map((v) => deepClean(v)) + } + if (val && typeof val === 'object') { + const out = {} + Object.keys(val).forEach((k) => { + if (k === 'meta' || k === 'id') return + out[k] = deepClean(val[k]) + }) + return out + } + return val + } + + // helper: normalize line endings by removing carriage returns to avoid Windows/Linux diffs + const normalizeCR = (val) => { + if (Array.isArray(val)) { + return val.map((v) => normalizeCR(v)) + } + if (val && typeof val === 'object') { + const out = {} + Object.keys(val).forEach((k) => { + out[k] = normalizeCR(val[k]) + }) + return out + } + if (typeof val === 'string') { + return val.replace(/\r/g, '') + } + return val + } + + // helper: expect actual to be a superset of expected (subset match) + const expectSubset = (actual, exp) => { + if (Array.isArray(exp)) { + expect(Array.isArray(actual)).toBe(true) + // Only check the first N items where N = exp.length + for (let i = 0; i < exp.length; i++) { + expectSubset(actual[i], exp[i]) + } + return + } + if (exp && typeof exp === 'object') { + expect(actual && typeof actual === 'object').toBe(true) + Object.keys(exp).forEach((k) => { + expectSubset(actual[k], exp[k]) + }) + return + } + // primitives + expect(actual).toEqual(exp) + } + + if (expected.error) { + expect(result.errors.length).toBeGreaterThan(0) + // 允许部分 schema 存在 + expect(result.schema).not.toBeUndefined() + } else { + expect(result.errors).toHaveLength(0) + expect(result.schema).toBeDefined() + const actualClean = deepClean(result.schema) + const expectedClean = deepClean(expected) + // Normalize CR to avoid Windows/Linux line ending diffs + const actualNorm = normalizeCR(actualClean) + const expectedNorm = normalizeCR(expectedClean) + // 进行部分匹配断言(忽略 meta/id 且仅要求包含期望结构) + expectSubset(actualNorm, expectedNorm) + } + }) + }) +}) diff --git a/packages/vue-to-dsl/tsconfig.json b/packages/vue-to-dsl/tsconfig.json new file mode 100644 index 0000000000..ac67ac085c --- /dev/null +++ b/packages/vue-to-dsl/tsconfig.json @@ -0,0 +1,20 @@ +{ + "compilerOptions": { + "target": "ES2020", + "module": "ESNext", + "moduleResolution": "Bundler", + "outDir": "dist", + "rootDir": "src", + "declaration": true, + "emitDeclarationOnly": true, + "skipLibCheck": true, + "esModuleInterop": true, + "allowSyntheticDefaultImports": true, + "forceConsistentCasingInFileNames": true, + "resolveJsonModule": true, + "strict": false, + "types": ["node"] + }, + "include": ["src/**/*.ts"], + "exclude": ["node_modules", "dist", "test"] +} diff --git a/packages/vue-to-dsl/vite.config.cli.mjs b/packages/vue-to-dsl/vite.config.cli.mjs new file mode 100644 index 0000000000..fd24027a48 --- /dev/null +++ b/packages/vue-to-dsl/vite.config.cli.mjs @@ -0,0 +1,32 @@ +import { defineConfig } from 'vite' +import path from 'node:path' + +// 专用于打包 CLI 入口 cli.ts,输出为 dist/cli.js(CJS) +export default defineConfig({ + build: { + lib: { + entry: path.resolve(__dirname, 'cli.ts'), + name: 'tiny-vue-to-dsl-cli', + formats: ['cjs'], + fileName: () => 'cli.cjs' + }, + rollupOptions: { + external: [ + 'fs', + 'fs/promises', + 'path', + 'url', + 'os', + 'jszip', + 'vue', + '@vue/compiler-sfc', + '@vue/compiler-dom', + '@babel/parser', + '@babel/traverse', + '@babel/types' + ] + }, + sourcemap: false, + emptyOutDir: false + } +}) diff --git a/packages/vue-to-dsl/vite.config.js b/packages/vue-to-dsl/vite.config.js new file mode 100644 index 0000000000..df1526a237 --- /dev/null +++ b/packages/vue-to-dsl/vite.config.js @@ -0,0 +1,41 @@ +import { defineConfig } from 'vite' + +export default defineConfig({ + build: { + lib: { + entry: 'src/index.ts', + name: 'TinyEngineVueToDsl', + formats: ['es', 'cjs'], + fileName: (format) => `tiny-engine-vue-to-dsl.${format === 'es' ? 'js' : format}` + }, + rollupOptions: { + external: [ + 'vue', + '@vue/compiler-sfc', + '@vue/compiler-dom', + '@babel/parser', + '@babel/traverse', + '@babel/types', + 'node:fs', + 'node:fs/promises', + 'node:path', + 'node:url' + ], + output: { + globals: { + vue: 'Vue', + '@vue/compiler-sfc': 'VueCompilerSFC', + '@vue/compiler-dom': 'VueCompilerDOM', + '@babel/parser': 'BabelParser', + '@babel/traverse': 'BabelTraverse', + '@babel/types': 'BabelTypes' + } + } + } + }, + resolve: { + alias: { + '@': '/src' + } + } +}) From c5356d885c32f70b04f8b33e1b70fcf936e15e13 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=9C=E5=8D=97dnf?= Date: Thu, 30 Oct 2025 20:34:21 +0800 Subject: [PATCH 2/9] test: add tests for data-binding & lifecycles (#1592) --- .../testcases/data-binding/advanced.test.js | 103 ++++ .../test/testcases/data-binding/basic.test.js | 70 +++ .../testcases/data-binding/edge-case.test.js | 122 +++++ .../test/testcases/data-binding/form.test.js | 140 ++++++ .../test/testcases/data-binding/mockData.js | 451 ++++++++++++++++++ .../generator/expected/appdemo01/package.json | 4 +- .../expected/appdemo01/src/router/index.js | 6 + .../appdemo01/src/views/LifeCyclePage.vue | 100 ++++ .../expected/appdemo01/src/views/createVm.vue | 62 ++- .../appdemo01/src/views/testCanvasRowCol.vue | 11 +- .../test/testcases/generator/mockData.js | 334 +++++++++++-- .../testcases/lifecycle/lifecycle.test.js | 100 ++++ .../test/testcases/lifecycle/mockData.js | 260 ++++++++++ 13 files changed, 1701 insertions(+), 62 deletions(-) create mode 100644 packages/vue-generator/test/testcases/data-binding/advanced.test.js create mode 100644 packages/vue-generator/test/testcases/data-binding/basic.test.js create mode 100644 packages/vue-generator/test/testcases/data-binding/edge-case.test.js create mode 100644 packages/vue-generator/test/testcases/data-binding/form.test.js create mode 100644 packages/vue-generator/test/testcases/data-binding/mockData.js create mode 100644 packages/vue-generator/test/testcases/generator/expected/appdemo01/src/views/LifeCyclePage.vue create mode 100644 packages/vue-generator/test/testcases/lifecycle/lifecycle.test.js create mode 100644 packages/vue-generator/test/testcases/lifecycle/mockData.js diff --git a/packages/vue-generator/test/testcases/data-binding/advanced.test.js b/packages/vue-generator/test/testcases/data-binding/advanced.test.js new file mode 100644 index 0000000000..ce2b53eb03 --- /dev/null +++ b/packages/vue-generator/test/testcases/data-binding/advanced.test.js @@ -0,0 +1,103 @@ +import { expect, test, describe } from 'vitest' +import path from 'path' +import fs from 'fs' +import { generateApp } from '@/generator/generateApp' +import { advancedDataBindingSchema } from './mockData' + +describe('Advanced Data Binding', () => { + test('should generate v-model for nested object bindings', async () => { + const instance = generateApp() + const res = await instance.generate(advancedDataBindingSchema) + const { genResult, errors } = res + + // 检查是否有错误 + expect(errors).toHaveLength(0) + + // 找到生成的 Vue 页面文件 + const vueFile = genResult.find((file) => file.fileName === 'AdvancedFormPage.vue') + expect(vueFile).toBeDefined() + + const content = vueFile.fileContent + + // 验证包含 v-model 绑定 + expect(content).toContain('v-model') + + // 验证嵌套对象数据绑定 + expect(content).toContain('v-model="state.formData.address.detail"') + expect(content).toContain('v-model="state.formData.address.zipCode"') + + // 验证基础字段绑定 + expect(content).toContain('v-model="state.formData.username"') + expect(content).toContain('v-model="state.formData.enabled"') + expect(content).toContain('v-model="state.formData.gender"') + expect(content).toContain('v-model="state.formData.remarks"') + expect(content).toContain('v-model="state.formData.category"') + + // 写入测试结果文件 + const outputDir = path.resolve(__dirname, './result/advanced') + fs.mkdirSync(outputDir, { recursive: true }) + + for (const { fileName, fileContent } of genResult) { + if (fileName.endsWith('.vue')) { + fs.writeFileSync(path.join(outputDir, fileName), fileContent) + } + } + }) + + test('should handle multiple component types with data binding', async () => { + const instance = generateApp() + const res = await instance.generate(advancedDataBindingSchema) + const { genResult } = res + + const vueFile = genResult.find((file) => file.fileName === 'AdvancedFormPage.vue') + const content = vueFile.fileContent + + // 统计 v-model 绑定数量 + const vModelMatches = content.match(/v-model[^=]*="[^"]+"/g) || [] + + // 应该有8个 v-model 绑定 + expect(vModelMatches).toHaveLength(8) + + // 验证所有组件类型都正确生成 + expect(content).toContain(' { + const instance = generateApp() + const res = await instance.generate(advancedDataBindingSchema) + const { genResult } = res + + const vueFile = genResult.find((file) => file.fileName === 'AdvancedFormPage.vue') + const content = vueFile.fileContent + + // 验证嵌套状态对象结构 + expect(content).toContain('formData: {') + expect(content).toContain('address: {') + expect(content).toContain("detail: ''") + expect(content).toContain("zipCode: ''") + + // 验证状态是响应式的 + expect(content).toContain('vue.reactive(') + }) + + test('should handle radio button groups correctly', async () => { + const instance = generateApp() + const res = await instance.generate(advancedDataBindingSchema) + const { genResult } = res + + const vueFile = genResult.find((file) => file.fileName === 'AdvancedFormPage.vue') + const content = vueFile.fileContent + + // 验证单选框组绑定到同一个字段 + const genderBindings = content.match(/v-model="state\.formData\.gender"/g) || [] + expect(genderBindings).toHaveLength(2) // 两个单选框绑定到同一字段 + + // 验证单选框的 value 属性 + expect(content).toContain('value="male"') + expect(content).toContain('value="female"') + }) +}) diff --git a/packages/vue-generator/test/testcases/data-binding/basic.test.js b/packages/vue-generator/test/testcases/data-binding/basic.test.js new file mode 100644 index 0000000000..f0d578fba5 --- /dev/null +++ b/packages/vue-generator/test/testcases/data-binding/basic.test.js @@ -0,0 +1,70 @@ +import { expect, test, describe } from 'vitest' +import path from 'path' +import fs from 'fs' +import { generateApp } from '@/generator/generateApp' +import { basicDataBindingSchema } from './mockData' + +describe('Basic Data Binding', () => { + test('should generate v-model for basic form components', async () => { + const instance = generateApp() + const res = await instance.generate(basicDataBindingSchema) + const { genResult, errors } = res + + // 检查是否有错误 + expect(errors).toHaveLength(0) + + // 找到生成的 Vue 页面文件 + const vueFile = genResult.find((file) => file.fileName === 'BasicDataBindingPage.vue') + expect(vueFile).toBeDefined() + + const content = vueFile.fileContent + + // 验证包含 v-model 绑定 + expect(content).toContain('v-model') + + // 验证具体的数据绑定 + expect(content).toContain('v-model="state.username"') + expect(content).toContain('v-model="state.role"') + expect(content).toContain('v-model="state.agreed"') + + // 验证组件正确渲染 + expect(content).toContain(' { + const instance = generateApp() + const res = await instance.generate(basicDataBindingSchema) + const { genResult } = res + + const vueFile = genResult.find((file) => file.fileName === 'BasicDataBindingPage.vue') + const content = vueFile.fileContent + + // 验证不同组件类型的 v-model 生成 + const vModelMatches = content.match(/v-model[^=]*="[^"]+"/g) || [] + + // 应该有3个 v-model 绑定 + expect(vModelMatches).toHaveLength(3) + + // 验证每个绑定都是正确的 + expect(vModelMatches).toContain('v-model="state.username"') + expect(vModelMatches).toContain('v-model="state.role"') + expect(vModelMatches).toContain('v-model="state.agreed"') + }) +}) diff --git a/packages/vue-generator/test/testcases/data-binding/edge-case.test.js b/packages/vue-generator/test/testcases/data-binding/edge-case.test.js new file mode 100644 index 0000000000..03c60e8af4 --- /dev/null +++ b/packages/vue-generator/test/testcases/data-binding/edge-case.test.js @@ -0,0 +1,122 @@ +import { expect, test, describe } from 'vitest' +import path from 'path' +import fs from 'fs' +import { generateApp } from '@/generator/generateApp' +import { edgeCaseDataBindingSchema } from './mockData' + +describe('Edge Case Data Binding Tests', () => { + test('should handle empty field gracefully', async () => { + const instance = generateApp() + const res = await instance.generate(edgeCaseDataBindingSchema) + const { genResult, errors } = res + + // 不应该因为空字段而报错 + expect(errors).toHaveLength(0) + + const vueFile = genResult.find((file) => file.fileName === 'EdgeCaseDataBindingPage.vue') + expect(vueFile).toBeDefined() + + const content = vueFile.fileContent + + // 空字段应该不生成v-model或生成空的v-model + // 确保不会导致语法错误 + expect(content).not.toContain('v-model=""') + expect(content).not.toContain('v-model="undefined"') + }) + + test('should remove this. prefix correctly', async () => { + const instance = generateApp() + const res = await instance.generate(edgeCaseDataBindingSchema) + const { genResult } = res + + const vueFile = genResult.find((file) => file.fileName === 'EdgeCaseDataBindingPage.vue') + const content = vueFile.fileContent + + // 应该移除this.前缀 + expect(content).toContain('v-model="state.withThis"') + expect(content).not.toContain('v-model="this.state.withThis"') + }) + + test('should handle deep nested field paths', async () => { + const instance = generateApp() + const res = await instance.generate(edgeCaseDataBindingSchema) + const { genResult } = res + + const vueFile = genResult.find((file) => file.fileName === 'EdgeCaseDataBindingPage.vue') + const content = vueFile.fileContent + + // 深层嵌套路径应该正确处理 + expect(content).toContain('v-model="state.level1.level2.level3.deepField"') + + // 验证状态结构正确生成 + expect(content).toContain('level1: {') + expect(content).toContain('level2: {') + expect(content).toContain('level3: {') + expect(content).toContain("deepField: ''") + }) + + test('should maintain correct state structure for nested objects', async () => { + const instance = generateApp() + const res = await instance.generate(edgeCaseDataBindingSchema) + const { genResult } = res + + const vueFile = genResult.find((file) => file.fileName === 'EdgeCaseDataBindingPage.vue') + const content = vueFile.fileContent + + // 验证响应式状态结构 + expect(content).toContain('vue.reactive(') + expect(content).toContain("noComponentType: ''") + expect(content).toContain("withThis: ''") + + // 写入测试结果 + const outputDir = path.resolve(__dirname, './result/edge-case') + fs.mkdirSync(outputDir, { recursive: true }) + + for (const { fileName, fileContent } of genResult) { + if (fileName.endsWith('.vue')) { + fs.writeFileSync(path.join(outputDir, fileName), fileContent) + } + } + }) + + test('should handle null and undefined values in JSDataBinding', async () => { + // 创建包含null/undefined值的特殊schema + const nullValueSchema = { + ...edgeCaseDataBindingSchema, + pageSchema: [ + { + ...edgeCaseDataBindingSchema.pageSchema[0], + children: [ + { + componentName: 'TinyInput', + props: { + modelValue: { + type: 'JSDataBinding', + value: null // null值 + } + } + }, + { + componentName: 'TinyInput', + props: { + modelValue: { + type: 'JSDataBinding', + value: undefined // undefined值 + } + } + } + ] + } + ] + } + + const instance = generateApp() + const res = await instance.generate(nullValueSchema) + + // 不应该因为null/undefined而抛出异常 + expect(res.errors).toHaveLength(0) + + const vueFile = res.genResult.find((file) => file.fileName === 'EdgeCaseDataBindingPage.vue') + expect(vueFile).toBeDefined() + }) +}) diff --git a/packages/vue-generator/test/testcases/data-binding/form.test.js b/packages/vue-generator/test/testcases/data-binding/form.test.js new file mode 100644 index 0000000000..a428a180ff --- /dev/null +++ b/packages/vue-generator/test/testcases/data-binding/form.test.js @@ -0,0 +1,140 @@ +import { expect, test, describe } from 'vitest' +import path from 'path' +import fs from 'fs' +import { generateApp } from '@/generator/generateApp' +import { complexFormDataBindingSchema } from './mockData' + +describe('Complex Form Data Binding Tests', () => { + test('should handle nested form sections with data binding', async () => { + const instance = generateApp() + const res = await instance.generate(complexFormDataBindingSchema) + const { genResult, errors } = res + + expect(errors).toHaveLength(0) + + const vueFile = genResult.find((file) => file.fileName === 'ComplexFormPage.vue') + expect(vueFile).toBeDefined() + + const content = vueFile.fileContent + + // 验证嵌套表单数据绑定 + expect(content).toContain('v-model="state.user.profile.name"') + expect(content).toContain('v-model="state.user.profile.email"') + expect(content).toContain('v-model="state.user.preferences.language"') + expect(content).toContain('v-model="state.user.preferences.emailNotifications"') + }) + + test('should generate correct nested state structure', async () => { + const instance = generateApp() + const res = await instance.generate(complexFormDataBindingSchema) + const { genResult } = res + + const vueFile = genResult.find((file) => file.fileName === 'ComplexFormPage.vue') + const content = vueFile.fileContent + + // 验证复杂嵌套状态结构 + expect(content).toContain('user: {') + expect(content).toContain('profile: {') + expect(content).toContain("name: ''") + expect(content).toContain("email: ''") + expect(content).toContain('preferences: {') + expect(content).toContain("language: ''") + expect(content).toContain('emailNotifications: false') + }) + + test('should handle multiple component types in complex form', async () => { + const instance = generateApp() + const res = await instance.generate(complexFormDataBindingSchema) + const { genResult } = res + + const vueFile = genResult.find((file) => file.fileName === 'ComplexFormPage.vue') + const content = vueFile.fileContent + + // 验证不同组件类型 + expect(content).toContain(' { + const instance = generateApp() + const res = await instance.generate(complexFormDataBindingSchema) + const { genResult } = res + + const vueFile = genResult.find((file) => file.fileName === 'ComplexFormPage.vue') + const content = vueFile.fileContent + + // 验证HTML结构和样式类 + expect(content).toContain('
') + expect(content).toContain('class="user-section"') + expect(content).toContain('class="preferences-section"') + + // 验证嵌套div结构 + expect(content).toMatch(/]*class="user-section"[^>]*>[\s\S]*/) + }) + + test('should count correct number of v-model bindings', async () => { + const instance = generateApp() + const res = await instance.generate(complexFormDataBindingSchema) + const { genResult } = res + + const vueFile = genResult.find((file) => file.fileName === 'ComplexFormPage.vue') + const content = vueFile.fileContent + + // 统计v-model数量 + const vModelMatches = content.match(/v-model[^=]*="[^"]+"/g) || [] + expect(vModelMatches).toHaveLength(4) // 4个表单字段 + + // 验证每个绑定都是唯一的 + const uniqueBindings = new Set(vModelMatches) + expect(uniqueBindings.size).toBe(4) + }) + + test('should handle different input types correctly', async () => { + const instance = generateApp() + const res = await instance.generate(complexFormDataBindingSchema) + const { genResult } = res + + const vueFile = genResult.find((file) => file.fileName === 'ComplexFormPage.vue') + const content = vueFile.fileContent + + // 验证不同输入类型的处理 + expect(content).toContain('placeholder="姓名"') + expect(content).toContain('placeholder="邮箱"') + expect(content).toContain('placeholder="语言偏好"') + + // 验证email类型特殊处理 + expect(content).toContain('type="email"') + + // 写入测试结果 + const outputDir = path.resolve(__dirname, './result/complex-form') + fs.mkdirSync(outputDir, { recursive: true }) + + for (const { fileName, fileContent } of genResult) { + if (fileName.endsWith('.vue')) { + fs.writeFileSync(path.join(outputDir, fileName), fileContent) + } + } + }) + + test('should generate proper component imports', async () => { + const instance = generateApp() + const res = await instance.generate(complexFormDataBindingSchema) + const { genResult } = res + + const vueFile = genResult.find((file) => file.fileName === 'ComplexFormPage.vue') + const content = vueFile.fileContent + + // 验证组件导入 + expect(content).toContain( + "import { Input as TinyInput, Select as TinySelect, Checkbox as TinyCheckbox } from '@opentiny/vue'" + ) + + // 验证Vue导入 + expect(content).toContain("import * as vue from 'vue'") + }) +}) diff --git a/packages/vue-generator/test/testcases/data-binding/mockData.js b/packages/vue-generator/test/testcases/data-binding/mockData.js new file mode 100644 index 0000000000..c5425a1a42 --- /dev/null +++ b/packages/vue-generator/test/testcases/data-binding/mockData.js @@ -0,0 +1,451 @@ +/** + * 数据绑定功能测试的模拟数据 + */ + +// 基础数据绑定测试 Schema +export const basicDataBindingSchema = { + meta: { + name: 'BasicDataBindingTest', + description: 'Test basic data binding functionality' + }, + componentsMap: [ + { + componentName: 'TinyInput', + exportName: 'Input', + package: '@opentiny/vue', + version: '^3.10.0', + destructuring: true + }, + { + componentName: 'TinySelect', + exportName: 'Select', + package: '@opentiny/vue', + version: '^3.10.0', + destructuring: true + } + ], + pageSchema: [ + { + componentName: 'div', + fileName: 'BasicDataBindingPage', + meta: { + id: 'basic-page', + isPage: true, + parentId: '0', + router: '/basic-data-binding' + }, + props: { + class: 'test-container' + }, + children: [ + { + componentName: 'TinyInput', + props: { + placeholder: '请输入用户名', + // 测试数据绑定 - 输入框绑定到用户名字段 + modelValue: { + type: 'JSExpression', + value: 'state.username', + model: true + } + } + }, + { + componentName: 'TinySelect', + props: { + placeholder: '请选择角色', + // 测试数据绑定 - 选择框绑定到角色字段 + modelValue: { + type: 'JSExpression', + value: 'state.role', + model: true + } + } + }, + { + componentName: 'input', + props: { + type: 'checkbox', + // 测试原生checkbox的数据绑定 + checked: { + type: 'JSExpression', + value: 'state.agreed', + model: true + } + } + } + ], + state: { + username: '', + role: '', + agreed: false + } + } + ], + blockSchema: [], + globalState: [], + dataSource: { list: [] }, + utils: [] +} + +// 高级数据绑定测试 Schema +export const advancedDataBindingSchema = { + meta: { + name: 'AdvancedDataBindingTest', + description: 'Test advanced data binding functionality with nested objects' + }, + componentsMap: [ + { + componentName: 'TinyInput', + exportName: 'Input', + package: '@opentiny/vue', + version: '^3.10.0', + destructuring: true + }, + { + componentName: 'TinySelect', + exportName: 'Select', + package: '@opentiny/vue', + version: '^3.10.0', + destructuring: true + }, + { + componentName: 'TinySwitch', + exportName: 'Switch', + package: '@opentiny/vue', + version: '^3.10.0', + destructuring: true + }, + { + componentName: 'TinyRadio', + exportName: 'Radio', + package: '@opentiny/vue', + version: '^3.10.0', + destructuring: true + } + ], + pageSchema: [ + { + componentName: 'div', + fileName: 'AdvancedFormPage', + meta: { + id: 'advanced-page', + isPage: true, + parentId: '0', + router: '/advanced-form' + }, + props: { + class: 'advanced-form-container' + }, + children: [ + { + componentName: 'TinyInput', + props: { + placeholder: '请输入用户名', + modelValue: { + type: 'JSExpression', + model: true, + value: 'state.formData.username' + } + } + }, + { + componentName: 'TinySwitch', + props: { + modelValue: { + type: 'JSExpression', + model: true, + value: 'state.formData.enabled' + } + } + }, + { + componentName: 'TinyRadio', + props: { + value: 'male', + checked: { + type: 'JSExpression', + model: true, + value: 'state.formData.gender' + } + } + }, + { + componentName: 'TinyRadio', + props: { + value: 'female', + checked: { + type: 'JSExpression', + model: true, + value: 'state.formData.gender' + } + } + }, + { + componentName: 'textarea', + props: { + placeholder: '请输入备注', + modelValue: { + type: 'JSExpression', + model: true, + value: 'state.formData.remarks' + } + } + }, + { + componentName: 'select', + props: { + modelValue: { + type: 'JSExpression', + model: true, + value: 'state.formData.category' + } + } + }, + { + componentName: 'TinyInput', + props: { + placeholder: '详细地址', + modelValue: { + type: 'JSExpression', + model: true, + value: 'state.formData.address.detail' + } + } + }, + { + componentName: 'TinyInput', + props: { + placeholder: '邮政编码', + modelValue: { + type: 'JSExpression', + model: true, + value: 'state.formData.address.zipCode' + } + } + } + ], + state: { + formData: { + username: '', + enabled: false, + gender: '', + remarks: '', + category: '', + address: { + detail: '', + zipCode: '' + } + } + } + } + ], + blockSchema: [], + globalState: [], + dataSource: { list: [] }, + utils: [] +} + +// 边界条件测试 Schema +export const edgeCaseDataBindingSchema = { + meta: { + name: 'EdgeCaseDataBindingTest', + description: 'Test edge cases and error handling for data binding' + }, + componentsMap: [ + { + componentName: 'TinyInput', + exportName: 'Input', + package: '@opentiny/vue', + version: '^3.10.0', + destructuring: true + } + ], + pageSchema: [ + { + componentName: 'div', + fileName: 'EdgeCaseDataBindingPage', + meta: { + id: 'edge-case-page', + isPage: true, + parentId: '0', + router: '/edge-case-data-binding' + }, + props: { + class: 'edge-case-container' + }, + children: [ + { + componentName: 'TinyInput', + props: { + placeholder: '空字段测试', + modelValue: { + type: 'JSExpression', + value: '' // 空字段 + } + } + }, + { + componentName: 'TinyInput', + props: { + placeholder: 'this前缀清理', + modelValue: { + type: 'JSExpression', + model: true, + value: 'this.state.withThis' + } + } + }, + { + componentName: 'TinyInput', + props: { + placeholder: '深层嵌套', + modelValue: { + type: 'JSExpression', + model: true, + value: 'state.level1.level2.level3.deepField' + } + } + } + ], + state: { + noComponentType: '', + withThis: '', + level1: { + level2: { + level3: { + deepField: '' + } + } + } + } + } + ], + blockSchema: [], + globalState: [], + dataSource: { list: [] }, + utils: [] +} + +// 复杂表单测试 Schema +export const complexFormDataBindingSchema = { + meta: { + name: 'ComplexFormDataBindingTest', + description: 'Test complex form with multiple data binding scenarios' + }, + componentsMap: [ + { + componentName: 'TinyInput', + exportName: 'Input', + package: '@opentiny/vue', + version: '^3.10.0', + destructuring: true + }, + { + componentName: 'TinySelect', + exportName: 'Select', + package: '@opentiny/vue', + version: '^3.10.0', + destructuring: true + }, + { + componentName: 'TinyCheckbox', + exportName: 'Checkbox', + package: '@opentiny/vue', + version: '^3.10.0', + destructuring: true + } + ], + pageSchema: [ + { + componentName: 'form', + fileName: 'ComplexFormPage', + meta: { + id: 'complex-form-page', + isPage: true, + parentId: '0', + router: '/complex-form' + }, + props: { + class: 'complex-form' + }, + children: [ + // 用户信息部分 + { + componentName: 'div', + props: { class: 'user-section' }, + children: [ + { + componentName: 'TinyInput', + props: { + placeholder: '姓名', + modelValue: { + type: 'JSExpression', + model: true, + value: 'state.user.profile.name' + } + } + }, + { + componentName: 'TinyInput', + props: { + type: 'email', + placeholder: '邮箱', + modelValue: { + type: 'JSExpression', + model: true, + value: 'state.user.profile.email' + } + } + } + ] + }, + // 偏好设置部分 + { + componentName: 'div', + props: { class: 'preferences-section' }, + children: [ + { + componentName: 'TinySelect', + props: { + placeholder: '语言偏好', + modelValue: { + type: 'JSExpression', + model: true, + value: 'state.user.preferences.language' + } + } + }, + { + componentName: 'TinyCheckbox', + props: { + label: '接收邮件通知', + checked: { + type: 'JSExpression', + model: true, + value: 'state.user.preferences.emailNotifications' + } + } + } + ] + } + ], + state: { + user: { + profile: { + name: '', + email: '' + }, + preferences: { + language: '', + emailNotifications: false + } + } + } + } + ], + blockSchema: [], + globalState: [], + dataSource: { list: [] }, + utils: [] +} diff --git a/packages/vue-generator/test/testcases/generator/expected/appdemo01/package.json b/packages/vue-generator/test/testcases/generator/expected/appdemo01/package.json index a404389d7b..6f3161e7ca 100644 --- a/packages/vue-generator/test/testcases/generator/expected/appdemo01/package.json +++ b/packages/vue-generator/test/testcases/generator/expected/appdemo01/package.json @@ -11,8 +11,8 @@ "module": "dist/index.js", "dependencies": { "@opentiny/tiny-engine-i18n-host": "^1.0.0", - "@opentiny/vue": "0.1.16", - "@opentiny/vue-icon": "0.1.16", + "@opentiny/vue": "3.24.0", + "@opentiny/vue-icon": "3.24.0", "axios": "latest", "axios-mock-adapter": "^1.19.0", "vue": "^3.3.9", diff --git a/packages/vue-generator/test/testcases/generator/expected/appdemo01/src/router/index.js b/packages/vue-generator/test/testcases/generator/expected/appdemo01/src/router/index.js index 53ad88c6ec..70c3c74f30 100644 --- a/packages/vue-generator/test/testcases/generator/expected/appdemo01/src/router/index.js +++ b/packages/vue-generator/test/testcases/generator/expected/appdemo01/src/router/index.js @@ -28,6 +28,12 @@ const routes = [ name: '3sV9KkvL3SuQIufS' } }, + { + name: 'lifecycle-page', + path: 'lifecycle', + component: () => import('@/views/LifeCyclePage.vue'), + children: [] + }, { name: '1737797330916', path: 'testCanvasRowCol', diff --git a/packages/vue-generator/test/testcases/generator/expected/appdemo01/src/views/LifeCyclePage.vue b/packages/vue-generator/test/testcases/generator/expected/appdemo01/src/views/LifeCyclePage.vue new file mode 100644 index 0000000000..fddf898275 --- /dev/null +++ b/packages/vue-generator/test/testcases/generator/expected/appdemo01/src/views/LifeCyclePage.vue @@ -0,0 +1,100 @@ + + + + diff --git a/packages/vue-generator/test/testcases/generator/expected/appdemo01/src/views/createVm.vue b/packages/vue-generator/test/testcases/generator/expected/appdemo01/src/views/createVm.vue index 57881bbb7a..7fae2266cb 100644 --- a/packages/vue-generator/test/testcases/generator/expected/appdemo01/src/views/createVm.vue +++ b/packages/vue-generator/test/testcases/generator/expected/appdemo01/src/views/createVm.vue @@ -55,7 +55,7 @@ > vCPUs 内存 - 规格名称 - + 规格名称 +
@@ -212,18 +212,18 @@ >
- + GiB IOPS上限240,IOPS突发上限5,000
- + GiB IOPS上限600,IOPS突发上限5,000 - +
@@ -340,7 +348,11 @@ 购买量 - + @@ -384,7 +396,8 @@ import { Form as TinyForm, Grid as TinyGrid, Select as TinySelect, - ButtonGroup as TinyButtonGroup + ButtonGroup as TinyButtonGroup, + Button as TinyButton } from '@opentiny/vue' import { IconPanelMini, IconPlus } from '@opentiny/vue-icon' import * as vue from 'vue' @@ -400,7 +413,24 @@ const { t, lowcodeWrap, stores } = vue.inject(I18nInjectionKey).lowcode() const wrap = lowcodeWrap(props, { emit }) wrap({ stores }) -const state = vue.reactive({ dataDisk: [1, 2, 3] }) +const state = vue.reactive({ + dataDisk: [1, 2, 3], + formData: { + zone: '1', + cpu: '1', + memory: '1', + storageType: '1', + storageSize: '40', + diskType: '1', + diskSize: '100', + networkType: '1', + bandwidth: '1', + instanceType: '1', + instanceCount: '1' + }, + inputValues: { diskLabel: '', systemDisk: '', dataDiskSize: '', networkConfig: '' }, + selectValues: { availableZone: '1', cpuArch: '1', memorySize: '1', storageOption: '1', networkOption: '1' } +}) wrap({ state }) diff --git a/packages/toolbars/generate-code/src/OverwriteDialog.vue b/packages/toolbars/upload/src/OverwriteDialog.vue similarity index 95% rename from packages/toolbars/generate-code/src/OverwriteDialog.vue rename to packages/toolbars/upload/src/OverwriteDialog.vue index 3702f6f9b7..6d2f326352 100644 --- a/packages/toolbars/generate-code/src/OverwriteDialog.vue +++ b/packages/toolbars/upload/src/OverwriteDialog.vue @@ -122,7 +122,7 @@ export default defineComponent({ .overwrite-dialog { .tip { font-size: 12px; - color: var(--te-configurator-common-text-color); + color: var(--te-toolbars-upload-text-color-primary); margin-bottom: 8px; } .actions { @@ -133,13 +133,13 @@ export default defineComponent({ } .switch-label { font-size: 12px; - color: var(--te-configurator-common-text-color); + color: var(--te-toolbars-upload-text-color-primary); } .list { max-height: 320px; overflow: auto; padding: 6px 4px; - border: 1px solid var(--te-configurator-common-border-color); + border: 1px solid var(--te-toolbars-upload-bg-color); border-radius: 4px; } .row { diff --git a/packages/toolbars/upload/src/http.ts b/packages/toolbars/upload/src/http.ts new file mode 100644 index 0000000000..ab0e50d2f4 --- /dev/null +++ b/packages/toolbars/upload/src/http.ts @@ -0,0 +1,18 @@ +/** + * 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. + * + */ + +/* metaService: engine.toolbars.upload.http */ + +import { getMetaApi, META_SERVICE } from '@opentiny/tiny-engine-meta-register' + +// 获取页面列表 +export const fetchPageList = (appId: string) => getMetaApi(META_SERVICE.Http).get(`/app-center/api/pages/list/${appId}`) diff --git a/packages/toolbars/upload/src/styles/vars.less b/packages/toolbars/upload/src/styles/vars.less new file mode 100644 index 0000000000..4f35f889b5 --- /dev/null +++ b/packages/toolbars/upload/src/styles/vars.less @@ -0,0 +1,12 @@ +:root { + --te-toolbars-upload-button-bg-color: var(--te-common-bg-prompt); + --te-toolbars-upload-text-color-primary: var(--te-common-text-primary); + --te-toolbars-upload-text-color-secondary: var(--te-common-text-secondary); + --te-toolbars-upload-icon-color: var(--te-common-icon-secondary); + --te-toolbars-upload-icon-color-primary: var(--te-common-icon-primary); + --te-toolbars-upload-bg-color-primary: var(--te-common-bg-primary); + --te-toolbars-upload-bg-color: var(--te-common-bg-default); + --te-toolbars-upload-bg-color-hover: var(--te-common-bg-container); + --te-toolbars-upload-border-color-checked: var(--te-common-border-checked); + --te-toolbars-upload-border-color-divider: var(--te-common-border-divider); +} diff --git a/packages/vue-generator/test/testcases/data-binding/advanced.test.js b/packages/vue-generator/test/testcases/data-binding/advanced.test.js deleted file mode 100644 index ce2b53eb03..0000000000 --- a/packages/vue-generator/test/testcases/data-binding/advanced.test.js +++ /dev/null @@ -1,103 +0,0 @@ -import { expect, test, describe } from 'vitest' -import path from 'path' -import fs from 'fs' -import { generateApp } from '@/generator/generateApp' -import { advancedDataBindingSchema } from './mockData' - -describe('Advanced Data Binding', () => { - test('should generate v-model for nested object bindings', async () => { - const instance = generateApp() - const res = await instance.generate(advancedDataBindingSchema) - const { genResult, errors } = res - - // 检查是否有错误 - expect(errors).toHaveLength(0) - - // 找到生成的 Vue 页面文件 - const vueFile = genResult.find((file) => file.fileName === 'AdvancedFormPage.vue') - expect(vueFile).toBeDefined() - - const content = vueFile.fileContent - - // 验证包含 v-model 绑定 - expect(content).toContain('v-model') - - // 验证嵌套对象数据绑定 - expect(content).toContain('v-model="state.formData.address.detail"') - expect(content).toContain('v-model="state.formData.address.zipCode"') - - // 验证基础字段绑定 - expect(content).toContain('v-model="state.formData.username"') - expect(content).toContain('v-model="state.formData.enabled"') - expect(content).toContain('v-model="state.formData.gender"') - expect(content).toContain('v-model="state.formData.remarks"') - expect(content).toContain('v-model="state.formData.category"') - - // 写入测试结果文件 - const outputDir = path.resolve(__dirname, './result/advanced') - fs.mkdirSync(outputDir, { recursive: true }) - - for (const { fileName, fileContent } of genResult) { - if (fileName.endsWith('.vue')) { - fs.writeFileSync(path.join(outputDir, fileName), fileContent) - } - } - }) - - test('should handle multiple component types with data binding', async () => { - const instance = generateApp() - const res = await instance.generate(advancedDataBindingSchema) - const { genResult } = res - - const vueFile = genResult.find((file) => file.fileName === 'AdvancedFormPage.vue') - const content = vueFile.fileContent - - // 统计 v-model 绑定数量 - const vModelMatches = content.match(/v-model[^=]*="[^"]+"/g) || [] - - // 应该有8个 v-model 绑定 - expect(vModelMatches).toHaveLength(8) - - // 验证所有组件类型都正确生成 - expect(content).toContain(' { - const instance = generateApp() - const res = await instance.generate(advancedDataBindingSchema) - const { genResult } = res - - const vueFile = genResult.find((file) => file.fileName === 'AdvancedFormPage.vue') - const content = vueFile.fileContent - - // 验证嵌套状态对象结构 - expect(content).toContain('formData: {') - expect(content).toContain('address: {') - expect(content).toContain("detail: ''") - expect(content).toContain("zipCode: ''") - - // 验证状态是响应式的 - expect(content).toContain('vue.reactive(') - }) - - test('should handle radio button groups correctly', async () => { - const instance = generateApp() - const res = await instance.generate(advancedDataBindingSchema) - const { genResult } = res - - const vueFile = genResult.find((file) => file.fileName === 'AdvancedFormPage.vue') - const content = vueFile.fileContent - - // 验证单选框组绑定到同一个字段 - const genderBindings = content.match(/v-model="state\.formData\.gender"/g) || [] - expect(genderBindings).toHaveLength(2) // 两个单选框绑定到同一字段 - - // 验证单选框的 value 属性 - expect(content).toContain('value="male"') - expect(content).toContain('value="female"') - }) -}) diff --git a/packages/vue-generator/test/testcases/data-binding/basic.test.js b/packages/vue-generator/test/testcases/data-binding/basic.test.js deleted file mode 100644 index f0d578fba5..0000000000 --- a/packages/vue-generator/test/testcases/data-binding/basic.test.js +++ /dev/null @@ -1,70 +0,0 @@ -import { expect, test, describe } from 'vitest' -import path from 'path' -import fs from 'fs' -import { generateApp } from '@/generator/generateApp' -import { basicDataBindingSchema } from './mockData' - -describe('Basic Data Binding', () => { - test('should generate v-model for basic form components', async () => { - const instance = generateApp() - const res = await instance.generate(basicDataBindingSchema) - const { genResult, errors } = res - - // 检查是否有错误 - expect(errors).toHaveLength(0) - - // 找到生成的 Vue 页面文件 - const vueFile = genResult.find((file) => file.fileName === 'BasicDataBindingPage.vue') - expect(vueFile).toBeDefined() - - const content = vueFile.fileContent - - // 验证包含 v-model 绑定 - expect(content).toContain('v-model') - - // 验证具体的数据绑定 - expect(content).toContain('v-model="state.username"') - expect(content).toContain('v-model="state.role"') - expect(content).toContain('v-model="state.agreed"') - - // 验证组件正确渲染 - expect(content).toContain(' { - const instance = generateApp() - const res = await instance.generate(basicDataBindingSchema) - const { genResult } = res - - const vueFile = genResult.find((file) => file.fileName === 'BasicDataBindingPage.vue') - const content = vueFile.fileContent - - // 验证不同组件类型的 v-model 生成 - const vModelMatches = content.match(/v-model[^=]*="[^"]+"/g) || [] - - // 应该有3个 v-model 绑定 - expect(vModelMatches).toHaveLength(3) - - // 验证每个绑定都是正确的 - expect(vModelMatches).toContain('v-model="state.username"') - expect(vModelMatches).toContain('v-model="state.role"') - expect(vModelMatches).toContain('v-model="state.agreed"') - }) -}) diff --git a/packages/vue-generator/test/testcases/data-binding/edge-case.test.js b/packages/vue-generator/test/testcases/data-binding/edge-case.test.js deleted file mode 100644 index 03c60e8af4..0000000000 --- a/packages/vue-generator/test/testcases/data-binding/edge-case.test.js +++ /dev/null @@ -1,122 +0,0 @@ -import { expect, test, describe } from 'vitest' -import path from 'path' -import fs from 'fs' -import { generateApp } from '@/generator/generateApp' -import { edgeCaseDataBindingSchema } from './mockData' - -describe('Edge Case Data Binding Tests', () => { - test('should handle empty field gracefully', async () => { - const instance = generateApp() - const res = await instance.generate(edgeCaseDataBindingSchema) - const { genResult, errors } = res - - // 不应该因为空字段而报错 - expect(errors).toHaveLength(0) - - const vueFile = genResult.find((file) => file.fileName === 'EdgeCaseDataBindingPage.vue') - expect(vueFile).toBeDefined() - - const content = vueFile.fileContent - - // 空字段应该不生成v-model或生成空的v-model - // 确保不会导致语法错误 - expect(content).not.toContain('v-model=""') - expect(content).not.toContain('v-model="undefined"') - }) - - test('should remove this. prefix correctly', async () => { - const instance = generateApp() - const res = await instance.generate(edgeCaseDataBindingSchema) - const { genResult } = res - - const vueFile = genResult.find((file) => file.fileName === 'EdgeCaseDataBindingPage.vue') - const content = vueFile.fileContent - - // 应该移除this.前缀 - expect(content).toContain('v-model="state.withThis"') - expect(content).not.toContain('v-model="this.state.withThis"') - }) - - test('should handle deep nested field paths', async () => { - const instance = generateApp() - const res = await instance.generate(edgeCaseDataBindingSchema) - const { genResult } = res - - const vueFile = genResult.find((file) => file.fileName === 'EdgeCaseDataBindingPage.vue') - const content = vueFile.fileContent - - // 深层嵌套路径应该正确处理 - expect(content).toContain('v-model="state.level1.level2.level3.deepField"') - - // 验证状态结构正确生成 - expect(content).toContain('level1: {') - expect(content).toContain('level2: {') - expect(content).toContain('level3: {') - expect(content).toContain("deepField: ''") - }) - - test('should maintain correct state structure for nested objects', async () => { - const instance = generateApp() - const res = await instance.generate(edgeCaseDataBindingSchema) - const { genResult } = res - - const vueFile = genResult.find((file) => file.fileName === 'EdgeCaseDataBindingPage.vue') - const content = vueFile.fileContent - - // 验证响应式状态结构 - expect(content).toContain('vue.reactive(') - expect(content).toContain("noComponentType: ''") - expect(content).toContain("withThis: ''") - - // 写入测试结果 - const outputDir = path.resolve(__dirname, './result/edge-case') - fs.mkdirSync(outputDir, { recursive: true }) - - for (const { fileName, fileContent } of genResult) { - if (fileName.endsWith('.vue')) { - fs.writeFileSync(path.join(outputDir, fileName), fileContent) - } - } - }) - - test('should handle null and undefined values in JSDataBinding', async () => { - // 创建包含null/undefined值的特殊schema - const nullValueSchema = { - ...edgeCaseDataBindingSchema, - pageSchema: [ - { - ...edgeCaseDataBindingSchema.pageSchema[0], - children: [ - { - componentName: 'TinyInput', - props: { - modelValue: { - type: 'JSDataBinding', - value: null // null值 - } - } - }, - { - componentName: 'TinyInput', - props: { - modelValue: { - type: 'JSDataBinding', - value: undefined // undefined值 - } - } - } - ] - } - ] - } - - const instance = generateApp() - const res = await instance.generate(nullValueSchema) - - // 不应该因为null/undefined而抛出异常 - expect(res.errors).toHaveLength(0) - - const vueFile = res.genResult.find((file) => file.fileName === 'EdgeCaseDataBindingPage.vue') - expect(vueFile).toBeDefined() - }) -}) diff --git a/packages/vue-generator/test/testcases/data-binding/form.test.js b/packages/vue-generator/test/testcases/data-binding/form.test.js deleted file mode 100644 index a428a180ff..0000000000 --- a/packages/vue-generator/test/testcases/data-binding/form.test.js +++ /dev/null @@ -1,140 +0,0 @@ -import { expect, test, describe } from 'vitest' -import path from 'path' -import fs from 'fs' -import { generateApp } from '@/generator/generateApp' -import { complexFormDataBindingSchema } from './mockData' - -describe('Complex Form Data Binding Tests', () => { - test('should handle nested form sections with data binding', async () => { - const instance = generateApp() - const res = await instance.generate(complexFormDataBindingSchema) - const { genResult, errors } = res - - expect(errors).toHaveLength(0) - - const vueFile = genResult.find((file) => file.fileName === 'ComplexFormPage.vue') - expect(vueFile).toBeDefined() - - const content = vueFile.fileContent - - // 验证嵌套表单数据绑定 - expect(content).toContain('v-model="state.user.profile.name"') - expect(content).toContain('v-model="state.user.profile.email"') - expect(content).toContain('v-model="state.user.preferences.language"') - expect(content).toContain('v-model="state.user.preferences.emailNotifications"') - }) - - test('should generate correct nested state structure', async () => { - const instance = generateApp() - const res = await instance.generate(complexFormDataBindingSchema) - const { genResult } = res - - const vueFile = genResult.find((file) => file.fileName === 'ComplexFormPage.vue') - const content = vueFile.fileContent - - // 验证复杂嵌套状态结构 - expect(content).toContain('user: {') - expect(content).toContain('profile: {') - expect(content).toContain("name: ''") - expect(content).toContain("email: ''") - expect(content).toContain('preferences: {') - expect(content).toContain("language: ''") - expect(content).toContain('emailNotifications: false') - }) - - test('should handle multiple component types in complex form', async () => { - const instance = generateApp() - const res = await instance.generate(complexFormDataBindingSchema) - const { genResult } = res - - const vueFile = genResult.find((file) => file.fileName === 'ComplexFormPage.vue') - const content = vueFile.fileContent - - // 验证不同组件类型 - expect(content).toContain(' { - const instance = generateApp() - const res = await instance.generate(complexFormDataBindingSchema) - const { genResult } = res - - const vueFile = genResult.find((file) => file.fileName === 'ComplexFormPage.vue') - const content = vueFile.fileContent - - // 验证HTML结构和样式类 - expect(content).toContain('
') - expect(content).toContain('class="user-section"') - expect(content).toContain('class="preferences-section"') - - // 验证嵌套div结构 - expect(content).toMatch(/]*class="user-section"[^>]*>[\s\S]*/) - }) - - test('should count correct number of v-model bindings', async () => { - const instance = generateApp() - const res = await instance.generate(complexFormDataBindingSchema) - const { genResult } = res - - const vueFile = genResult.find((file) => file.fileName === 'ComplexFormPage.vue') - const content = vueFile.fileContent - - // 统计v-model数量 - const vModelMatches = content.match(/v-model[^=]*="[^"]+"/g) || [] - expect(vModelMatches).toHaveLength(4) // 4个表单字段 - - // 验证每个绑定都是唯一的 - const uniqueBindings = new Set(vModelMatches) - expect(uniqueBindings.size).toBe(4) - }) - - test('should handle different input types correctly', async () => { - const instance = generateApp() - const res = await instance.generate(complexFormDataBindingSchema) - const { genResult } = res - - const vueFile = genResult.find((file) => file.fileName === 'ComplexFormPage.vue') - const content = vueFile.fileContent - - // 验证不同输入类型的处理 - expect(content).toContain('placeholder="姓名"') - expect(content).toContain('placeholder="邮箱"') - expect(content).toContain('placeholder="语言偏好"') - - // 验证email类型特殊处理 - expect(content).toContain('type="email"') - - // 写入测试结果 - const outputDir = path.resolve(__dirname, './result/complex-form') - fs.mkdirSync(outputDir, { recursive: true }) - - for (const { fileName, fileContent } of genResult) { - if (fileName.endsWith('.vue')) { - fs.writeFileSync(path.join(outputDir, fileName), fileContent) - } - } - }) - - test('should generate proper component imports', async () => { - const instance = generateApp() - const res = await instance.generate(complexFormDataBindingSchema) - const { genResult } = res - - const vueFile = genResult.find((file) => file.fileName === 'ComplexFormPage.vue') - const content = vueFile.fileContent - - // 验证组件导入 - expect(content).toContain( - "import { Input as TinyInput, Select as TinySelect, Checkbox as TinyCheckbox } from '@opentiny/vue'" - ) - - // 验证Vue导入 - expect(content).toContain("import * as vue from 'vue'") - }) -}) diff --git a/packages/vue-generator/test/testcases/data-binding/mockData.js b/packages/vue-generator/test/testcases/data-binding/mockData.js deleted file mode 100644 index c5425a1a42..0000000000 --- a/packages/vue-generator/test/testcases/data-binding/mockData.js +++ /dev/null @@ -1,451 +0,0 @@ -/** - * 数据绑定功能测试的模拟数据 - */ - -// 基础数据绑定测试 Schema -export const basicDataBindingSchema = { - meta: { - name: 'BasicDataBindingTest', - description: 'Test basic data binding functionality' - }, - componentsMap: [ - { - componentName: 'TinyInput', - exportName: 'Input', - package: '@opentiny/vue', - version: '^3.10.0', - destructuring: true - }, - { - componentName: 'TinySelect', - exportName: 'Select', - package: '@opentiny/vue', - version: '^3.10.0', - destructuring: true - } - ], - pageSchema: [ - { - componentName: 'div', - fileName: 'BasicDataBindingPage', - meta: { - id: 'basic-page', - isPage: true, - parentId: '0', - router: '/basic-data-binding' - }, - props: { - class: 'test-container' - }, - children: [ - { - componentName: 'TinyInput', - props: { - placeholder: '请输入用户名', - // 测试数据绑定 - 输入框绑定到用户名字段 - modelValue: { - type: 'JSExpression', - value: 'state.username', - model: true - } - } - }, - { - componentName: 'TinySelect', - props: { - placeholder: '请选择角色', - // 测试数据绑定 - 选择框绑定到角色字段 - modelValue: { - type: 'JSExpression', - value: 'state.role', - model: true - } - } - }, - { - componentName: 'input', - props: { - type: 'checkbox', - // 测试原生checkbox的数据绑定 - checked: { - type: 'JSExpression', - value: 'state.agreed', - model: true - } - } - } - ], - state: { - username: '', - role: '', - agreed: false - } - } - ], - blockSchema: [], - globalState: [], - dataSource: { list: [] }, - utils: [] -} - -// 高级数据绑定测试 Schema -export const advancedDataBindingSchema = { - meta: { - name: 'AdvancedDataBindingTest', - description: 'Test advanced data binding functionality with nested objects' - }, - componentsMap: [ - { - componentName: 'TinyInput', - exportName: 'Input', - package: '@opentiny/vue', - version: '^3.10.0', - destructuring: true - }, - { - componentName: 'TinySelect', - exportName: 'Select', - package: '@opentiny/vue', - version: '^3.10.0', - destructuring: true - }, - { - componentName: 'TinySwitch', - exportName: 'Switch', - package: '@opentiny/vue', - version: '^3.10.0', - destructuring: true - }, - { - componentName: 'TinyRadio', - exportName: 'Radio', - package: '@opentiny/vue', - version: '^3.10.0', - destructuring: true - } - ], - pageSchema: [ - { - componentName: 'div', - fileName: 'AdvancedFormPage', - meta: { - id: 'advanced-page', - isPage: true, - parentId: '0', - router: '/advanced-form' - }, - props: { - class: 'advanced-form-container' - }, - children: [ - { - componentName: 'TinyInput', - props: { - placeholder: '请输入用户名', - modelValue: { - type: 'JSExpression', - model: true, - value: 'state.formData.username' - } - } - }, - { - componentName: 'TinySwitch', - props: { - modelValue: { - type: 'JSExpression', - model: true, - value: 'state.formData.enabled' - } - } - }, - { - componentName: 'TinyRadio', - props: { - value: 'male', - checked: { - type: 'JSExpression', - model: true, - value: 'state.formData.gender' - } - } - }, - { - componentName: 'TinyRadio', - props: { - value: 'female', - checked: { - type: 'JSExpression', - model: true, - value: 'state.formData.gender' - } - } - }, - { - componentName: 'textarea', - props: { - placeholder: '请输入备注', - modelValue: { - type: 'JSExpression', - model: true, - value: 'state.formData.remarks' - } - } - }, - { - componentName: 'select', - props: { - modelValue: { - type: 'JSExpression', - model: true, - value: 'state.formData.category' - } - } - }, - { - componentName: 'TinyInput', - props: { - placeholder: '详细地址', - modelValue: { - type: 'JSExpression', - model: true, - value: 'state.formData.address.detail' - } - } - }, - { - componentName: 'TinyInput', - props: { - placeholder: '邮政编码', - modelValue: { - type: 'JSExpression', - model: true, - value: 'state.formData.address.zipCode' - } - } - } - ], - state: { - formData: { - username: '', - enabled: false, - gender: '', - remarks: '', - category: '', - address: { - detail: '', - zipCode: '' - } - } - } - } - ], - blockSchema: [], - globalState: [], - dataSource: { list: [] }, - utils: [] -} - -// 边界条件测试 Schema -export const edgeCaseDataBindingSchema = { - meta: { - name: 'EdgeCaseDataBindingTest', - description: 'Test edge cases and error handling for data binding' - }, - componentsMap: [ - { - componentName: 'TinyInput', - exportName: 'Input', - package: '@opentiny/vue', - version: '^3.10.0', - destructuring: true - } - ], - pageSchema: [ - { - componentName: 'div', - fileName: 'EdgeCaseDataBindingPage', - meta: { - id: 'edge-case-page', - isPage: true, - parentId: '0', - router: '/edge-case-data-binding' - }, - props: { - class: 'edge-case-container' - }, - children: [ - { - componentName: 'TinyInput', - props: { - placeholder: '空字段测试', - modelValue: { - type: 'JSExpression', - value: '' // 空字段 - } - } - }, - { - componentName: 'TinyInput', - props: { - placeholder: 'this前缀清理', - modelValue: { - type: 'JSExpression', - model: true, - value: 'this.state.withThis' - } - } - }, - { - componentName: 'TinyInput', - props: { - placeholder: '深层嵌套', - modelValue: { - type: 'JSExpression', - model: true, - value: 'state.level1.level2.level3.deepField' - } - } - } - ], - state: { - noComponentType: '', - withThis: '', - level1: { - level2: { - level3: { - deepField: '' - } - } - } - } - } - ], - blockSchema: [], - globalState: [], - dataSource: { list: [] }, - utils: [] -} - -// 复杂表单测试 Schema -export const complexFormDataBindingSchema = { - meta: { - name: 'ComplexFormDataBindingTest', - description: 'Test complex form with multiple data binding scenarios' - }, - componentsMap: [ - { - componentName: 'TinyInput', - exportName: 'Input', - package: '@opentiny/vue', - version: '^3.10.0', - destructuring: true - }, - { - componentName: 'TinySelect', - exportName: 'Select', - package: '@opentiny/vue', - version: '^3.10.0', - destructuring: true - }, - { - componentName: 'TinyCheckbox', - exportName: 'Checkbox', - package: '@opentiny/vue', - version: '^3.10.0', - destructuring: true - } - ], - pageSchema: [ - { - componentName: 'form', - fileName: 'ComplexFormPage', - meta: { - id: 'complex-form-page', - isPage: true, - parentId: '0', - router: '/complex-form' - }, - props: { - class: 'complex-form' - }, - children: [ - // 用户信息部分 - { - componentName: 'div', - props: { class: 'user-section' }, - children: [ - { - componentName: 'TinyInput', - props: { - placeholder: '姓名', - modelValue: { - type: 'JSExpression', - model: true, - value: 'state.user.profile.name' - } - } - }, - { - componentName: 'TinyInput', - props: { - type: 'email', - placeholder: '邮箱', - modelValue: { - type: 'JSExpression', - model: true, - value: 'state.user.profile.email' - } - } - } - ] - }, - // 偏好设置部分 - { - componentName: 'div', - props: { class: 'preferences-section' }, - children: [ - { - componentName: 'TinySelect', - props: { - placeholder: '语言偏好', - modelValue: { - type: 'JSExpression', - model: true, - value: 'state.user.preferences.language' - } - } - }, - { - componentName: 'TinyCheckbox', - props: { - label: '接收邮件通知', - checked: { - type: 'JSExpression', - model: true, - value: 'state.user.preferences.emailNotifications' - } - } - } - ] - } - ], - state: { - user: { - profile: { - name: '', - email: '' - }, - preferences: { - language: '', - emailNotifications: false - } - } - } - } - ], - blockSchema: [], - globalState: [], - dataSource: { list: [] }, - utils: [] -} diff --git a/packages/vue-generator/test/testcases/generator/expected/appdemo01/package.json b/packages/vue-generator/test/testcases/generator/expected/appdemo01/package.json index 6f3161e7ca..a404389d7b 100644 --- a/packages/vue-generator/test/testcases/generator/expected/appdemo01/package.json +++ b/packages/vue-generator/test/testcases/generator/expected/appdemo01/package.json @@ -11,8 +11,8 @@ "module": "dist/index.js", "dependencies": { "@opentiny/tiny-engine-i18n-host": "^1.0.0", - "@opentiny/vue": "3.24.0", - "@opentiny/vue-icon": "3.24.0", + "@opentiny/vue": "0.1.16", + "@opentiny/vue-icon": "0.1.16", "axios": "latest", "axios-mock-adapter": "^1.19.0", "vue": "^3.3.9", diff --git a/packages/vue-generator/test/testcases/generator/expected/appdemo01/src/router/index.js b/packages/vue-generator/test/testcases/generator/expected/appdemo01/src/router/index.js index 70c3c74f30..53ad88c6ec 100644 --- a/packages/vue-generator/test/testcases/generator/expected/appdemo01/src/router/index.js +++ b/packages/vue-generator/test/testcases/generator/expected/appdemo01/src/router/index.js @@ -28,12 +28,6 @@ const routes = [ name: '3sV9KkvL3SuQIufS' } }, - { - name: 'lifecycle-page', - path: 'lifecycle', - component: () => import('@/views/LifeCyclePage.vue'), - children: [] - }, { name: '1737797330916', path: 'testCanvasRowCol', diff --git a/packages/vue-generator/test/testcases/generator/expected/appdemo01/src/views/LifeCyclePage.vue b/packages/vue-generator/test/testcases/generator/expected/appdemo01/src/views/LifeCyclePage.vue deleted file mode 100644 index fddf898275..0000000000 --- a/packages/vue-generator/test/testcases/generator/expected/appdemo01/src/views/LifeCyclePage.vue +++ /dev/null @@ -1,100 +0,0 @@ - - - - diff --git a/packages/vue-generator/test/testcases/generator/expected/appdemo01/src/views/createVm.vue b/packages/vue-generator/test/testcases/generator/expected/appdemo01/src/views/createVm.vue index 7fae2266cb..57881bbb7a 100644 --- a/packages/vue-generator/test/testcases/generator/expected/appdemo01/src/views/createVm.vue +++ b/packages/vue-generator/test/testcases/generator/expected/appdemo01/src/views/createVm.vue @@ -55,7 +55,7 @@ > vCPUs 内存 - 规格名称 - + 规格名称 +
@@ -212,18 +212,18 @@ >
- + GiB IOPS上限240,IOPS突发上限5,000
- + GiB IOPS上限600,IOPS突发上限5,000 - +
@@ -348,11 +340,7 @@ 购买量 - + @@ -396,8 +384,7 @@ import { Form as TinyForm, Grid as TinyGrid, Select as TinySelect, - ButtonGroup as TinyButtonGroup, - Button as TinyButton + ButtonGroup as TinyButtonGroup } from '@opentiny/vue' import { IconPanelMini, IconPlus } from '@opentiny/vue-icon' import * as vue from 'vue' @@ -413,24 +400,7 @@ const { t, lowcodeWrap, stores } = vue.inject(I18nInjectionKey).lowcode() const wrap = lowcodeWrap(props, { emit }) wrap({ stores }) -const state = vue.reactive({ - dataDisk: [1, 2, 3], - formData: { - zone: '1', - cpu: '1', - memory: '1', - storageType: '1', - storageSize: '40', - diskType: '1', - diskSize: '100', - networkType: '1', - bandwidth: '1', - instanceType: '1', - instanceCount: '1' - }, - inputValues: { diskLabel: '', systemDisk: '', dataDiskSize: '', networkConfig: '' }, - selectValues: { availableZone: '1', cpuArch: '1', memorySize: '1', storageOption: '1', networkOption: '1' } -}) +const state = vue.reactive({ dataDisk: [1, 2, 3] }) wrap({ state })