Skip to content

feat: introduce vue-to-dsl plugin#1590

Merged
hexqi merged 65 commits intoopentiny:ospp-2025/source-to-dslfrom
zjy2414:source-to-dsl/vue-to-dsl
Oct 29, 2025
Merged

feat: introduce vue-to-dsl plugin#1590
hexqi merged 65 commits intoopentiny:ospp-2025/source-to-dslfrom
zjy2414:source-to-dsl/vue-to-dsl

Conversation

@zjy2414
Copy link

@zjy2414 zjy2414 commented Aug 23, 2025

English | 简体中文

PR

PR Checklist

Please check if your PR fulfills the following requirements:

  • The commit message follows our Commit Message Guidelines
  • Tests for the changes have been added (for bug fixes / features)
  • Docs have been added / updated (for bug fixes / features)
  • Built its own designer, fully self-validated

PR Type

What kind of change does this PR introduce?

  • Feature
  • Bugfix
  • Code style update (formatting, local variables)
  • Refactoring (no functional changes, no api changes)
  • Build related changes
  • CI related changes
  • Documentation content changes
  • Other... Please describe:

Background and solution

What is the current behavior?

项目缺少从 Vue 源码到 Tiny Engine DSL 的转换能力。

What is the new behavior?

本 PR 新增 vue-to-dsl 插件并围绕解析与生成流程进行增强,核心包括:

  • 新增 vue-to-dsl 插件与完整转换流程,将 Vue SFC 转成 Tiny Engine DSL schema。
  • 统一将箭头函数转换为具名函数,便于后续序列化与引用。
  • 新增 AST 值转换能力,覆盖多种字面量与常见表达式。
  • 递归分配组件 ID,保证 schema 中组件引用稳定。
  • 规范化 tiny-icon-* 成通用 Icon 组件,统一组件命名。
  • 将事件处理规范转换为 JS 表达式形态,便于渲染与运行时执行。
  • 补充测试用例(SFC、Options API、<script setup>、完整Vue项目转换等)。
  • 在ToolBar添加vue文件/vue项目导入的按钮

Does this PR introduce a breaking change?

  • Yes
  • No

Summary by CodeRabbit

  • New Features

    • New Vue-to-DSL converter package with CLI: convert Vue SFCs (single file, directory or zip) into TinyEngine DSL schemas; supports app-level aggregation (i18n, data sources, global state, pages).
    • Generate Code toolbar: updated icons and new import UI (upload Vue file / directory / zip) with overwrite dialog and granular upload flow.
  • Documentation

    • README added with usage, CLI, API examples and quick start.
  • Tests

    • Unit and end-to-end tests and fixtures validating conversion scenarios.

@github-actions github-actions bot added enhancement New feature or request ospp ospp labels Aug 23, 2025
@coderabbitai
Copy link
Contributor

coderabbitai bot commented Aug 23, 2025

Walkthrough

Adds a new package packages/vue-to-dsl implementing a Vue SFC → TinyEngine DSL converter (library + CLI), parsers, schema generator, tests, demo app/fixtures, Vite configs, and integrates import/upload UI into the generate-code toolbar with overwrite handling.

Changes

Cohort / File(s) Summary
Package config & build
packages/vue-to-dsl/package.json, packages/vue-to-dsl/tsconfig.json, packages/vue-to-dsl/vite.config.js, packages/vue-to-dsl/vite.config.cli.mjs, packages/vue-to-dsl/.gitignore
New package metadata, publish/build scripts, TypeScript config, library and CLI Vite configs, and .gitignore (ignores output/).
Public API / entry
packages/vue-to-dsl/src/index.ts, packages/vue-to-dsl/src/index.d.ts, packages/vue-to-dsl/src/types/index.js
New entrypoint re-exports, TypeScript declaration module and JSDoc typedefs describing VueToDslConverter, parse/generate functions, and schema types.
Core converter
packages/vue-to-dsl/src/converter.ts
New VueToDslConverter implementation supporting convertFromString/File/Multiple, convertAppDirectory, convertAppFromZip, options management, dependency collection, errors/warnings, and app-level enrichment.
Schema generation & constants
packages/vue-to-dsl/src/generator/index.ts, packages/vue-to-dsl/src/constants.ts
New generateSchema and generateAppSchema implementations and default component/component map constants.
SFC parsing
packages/vue-to-dsl/src/parser/index.ts
parseSFC, parseVueFile, validateSFC, getSFCMeta using @vue/compiler-sfc to decompose SFC parts and metadata.
Parsers (template/script/style)
packages/vue-to-dsl/src/parsers/templateParser.ts, packages/vue-to-dsl/src/parsers/scriptParser.ts, packages/vue-to-dsl/src/parsers/styleParser.ts, packages/vue-to-dsl/src/parsers/index.ts
New template AST→DSL translator, Babel-based script parser (Options + Composition), CSS/style utilities, and consolidated parser exports.
CLI
packages/vue-to-dsl/cli.ts, packages/vue-to-dsl/vite.config.cli.mjs
Node ESM CLI to convert .vue → schema (json/js), with format/output options and diagnostics; build config for CLI bundle.
Tests & fixtures (single-file)
packages/vue-to-dsl/test/sfc/*, packages/vue-to-dsl/test/testcases/*
Vitest unit tests and multiple SFC input/expected fixtures covering simple, createVM, login, dashboard, survey, lifecycle scenarios and testcase discovery runner.
Tests & fixtures (app-level)
packages/vue-to-dsl/test/full/*, packages/vue-to-dsl/test/full/input/appdemo01/*, packages/vue-to-dsl/test/full/expected/*
Demo Vite+Vue3 app (router, stores, i18n, http, lowcodeConfig, utils, views), zip fixture, and expected app-level schemas.
Dev alias & workspace updates
packages/build/vite-config/src/vite-plugins/devAliasPlugin.js, packages/toolbars/generate-code/package.json
Adds dev-time alias for @opentiny/tiny-engine-vue-to-dsl and workspace dependency in generate-code package.json.
Generate-code toolbar UI changes
packages/toolbars/generate-code/meta.js, packages/toolbars/generate-code/src/Main.vue, packages/toolbars/generate-code/src/OverwriteDialog.vue
Splits icon keys, adds upload UI (file/dir/zip), wiring to VueToDslConverter, duplicate-page overwrite dialog and related upload/overwrite flows.

Sequence Diagram(s)

sequenceDiagram
    participant U as User
    participant UI as Generate-Code Toolbar
    participant C as VueToDslConverter
    participant G as Generator
    participant S as Page Store
    rect rgba(230,247,255,0.6)
    note right of UI: Upload (file / directory / zip)
    end

    U->>UI: Click Import → choose File/Directory/Zip
    UI->>C: invoke convertFromFile / convertAppDirectory / convertAppFromZip
    C->>C: parse SFC(s) (template, script, style)
    C->>G: generateSchema / generateAppSchema
    G-->>C: PageSchema / AppSchema
    C-->>UI: ConvertResult (schema, errors, warnings, dependencies)

    alt Duplicate page names found
        UI->>UI: show OverwriteDialog
        U->>UI: confirm selection
        UI->>S: overwrite existing pages or create new pages
    else
        UI->>S: create new pages and sync app-level data (i18n, dataSource, globalState)
    end

    UI->>S: update page tree and navigate to new/updated page
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~75 minutes

Areas to focus review on:

  • Parsers: templateParser.ts, scriptParser.ts, styleParser.ts (logic-heavy AST transforms and Babel usage).
  • Converter app-level aggregation: converter.ts (zip handling, i18n/dataSource extraction).
  • Generator: generator/index.ts (schema assembly, ID assignment, sanitization).
  • Tests/fixtures size: test/full/expected/* and many fixtures (validate test assumptions and stability).
  • Integration points: packages/toolbars/generate-code/src/Main.vue (upload flow) and dev alias plugin change.

Poem

🐰 I nibble code in moonlit stacks,

I hop through templates, scripts, and tracks,
From SFC whispers to DSL's bright tune,
I stitch pages, i18n, and data soon.
Import, convert — the engine hums anew!

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 26.67% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title Check ✅ Passed The title "feat: introduce vue-to-dsl plugin" accurately reflects the primary change in this PR. The changeset introduces a comprehensive new package @opentiny/tiny-engine-vue-to-dsl with complete conversion infrastructure, including a converter class, parser modules, generator functions, CLI tool, comprehensive tests, and integration into existing components. The title uses clear, specific terminology ("vue-to-dsl plugin") that conveys the main purpose without vague phrasing. A developer scanning the git history would immediately understand that this PR adds a Vue-to-DSL conversion capability to the project. The title is appropriately concise while still being descriptive of the core functionality being introduced.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 36

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
packages/vue-to-dsl/test/testcases/005_createVM/expected/schema.json (1)

1-1129: Fix serialization anomalies in 005_createVM fixture

The expected schema for packages/vue-to-dsl/test/testcases/005_createVM/expected/schema.json contains several serialization issues that must be corrected before re-generating the fixture:

  • [object Event] artifact in a style prop (line 123)
  • Duplicate prop casings for labels (labelPosition vs. label-position, plus repeated label-width) at lines 65–67, 203–205, 542–544, 671–673, 750–752, 911–913
  • Numeric props emitted as strings rather than numbers:
    • "active": "2" (line 31)
    • "span": "8" (line 929), "span": "5" (line 941), "span": "7" (line 991), "span": "4" (line 1100)

Please update the Vue-to-DSL generator to:

  • Properly serialize style values (avoid [object Event])
  • Normalize prop naming to a single casing (camelCase or kebab-case per DSL conventions)
  • Emit numeric prop values as actual numbers

After making these changes, re-generate the fixture and verify that no other testcases exhibit similar anomalies.

🧹 Nitpick comments (47)
packages/vue-to-dsl/.gitignore (1)

1-1: Consider broadening ignore patterns to avoid committing build/test artifacts.

Currently only output/ is ignored. Recommend also ignoring dist/ and any testcase-local outputs to prevent accidental commits of generated files.

Apply this diff:

-output/
+output/
+dist/
+test/**/output/
packages/vue-to-dsl/tsconfig.json (1)

15-15: Enable strict mode (or a subset) for better API safety.

Given this is a library intended for external consumption, recommend turning on stricter checks (at least noImplicitAny) to catch issues early.

Suggested minimal diff:

-    "strict": false,
+    "strict": true,
+    "noImplicitOverride": true,
+    "noUncheckedIndexedAccess": true,
packages/vue-to-dsl/vite.config.js (5)

1-1: Add imports for URL-based path resolution (used in alias fix below).

Apply this diff to add imports at the top:

-import { defineConfig } from 'vite'
+import { defineConfig } from 'vite'
+import { fileURLToPath, URL } from 'node:url'
+// Optional if you prefer path.resolve instead of URL:
+// import { resolve } from 'node:path'
+// Optional: if you adopt vite-plugin-dts per tsconfig comment:
+// import dts from 'vite-plugin-dts'

12-23: Externalize both node:-prefixed and bare Node built-ins.

Code may import 'fs' or 'path' without the 'node:' scheme. Add both forms to avoid bundling Node built-ins by accident.

       external: [
         'vue',
         '@vue/compiler-sfc',
         '@vue/compiler-dom',
         '@babel/parser',
         '@babel/traverse',
         '@babel/types',
         'node:fs',
         'node:fs/promises',
         'node:path',
-        'node:url'
+        'node:url',
+        'fs',
+        'path',
+        'url'
       ],

25-33: Globals mapping is unused for ES/CJS outputs.

rollupOptions.output.globals only applies to UMD/IIFE. Either remove this block or add 'umd' to formats if a UMD build is desired.

Option A (remove):

-      output: {
-        globals: {
-          vue: 'Vue',
-          '@vue/compiler-sfc': 'VueCompilerSFC',
-          '@vue/compiler-dom': 'VueCompilerDOM',
-          '@babel/parser': 'BabelParser',
-          '@babel/traverse': 'BabelTraverse',
-          '@babel/types': 'BabelTypes'
-        }
-      }
+      // No globals needed for ES/CJS

Option B (produce UMD too):

-      formats: ['es', 'cjs'],
+      formats: ['es', 'cjs', 'umd'],

Keep the existing globals if you choose UMD.


5-10: Consider ESM filename extension and source maps.

Some consumers prefer .mjs for ESM artifacts, and source maps aid debugging.

-      fileName: (format) => `tiny-engine-vue-to-dsl.${format === 'es' ? 'js' : format}`
+      fileName: (format) => `tiny-engine-vue-to-dsl.${format === 'es' ? 'mjs' : 'cjs'}`
     },
+    sourcemap: true,

3-41: Emit .d.ts during the Vite build (matches tsconfig intent).

If you don’t already run tsc separately, add vite-plugin-dts so type declarations ship with the library.

Diff within this file:

-import { defineConfig } from 'vite'
+import { defineConfig } from 'vite'
+import dts from 'vite-plugin-dts'
@@
-export default defineConfig({
+export default defineConfig({
+  plugins: [
+    dts({
+      // Generates combined .d.ts in dist and respects tsconfig include/exclude
+      rollupTypes: true
+    })
+  ],
packages/vue-to-dsl/test/testcases/003_options_api/expected/schema.json (1)

14-17: Normalize function string to named function syntax

For consistency with other fixtures (e.g., 004) and your stated goal to unify arrow → named functions, prefer function updateMessage() over method shorthand in the serialized function string.

   "methods": {
     "updateMessage": {
       "type": "JSFunction",
-      "value": "updateMessage() { this.message = 'Updated!' }"
+      "value": "function updateMessage() { this.message = 'Updated!' }"
     }
   },
packages/vue-to-dsl/test/testcases/001_simple/expected/schema.json (1)

7-8: Stability of generatedAt in tests

If the converter emits dynamic timestamps, assert that tests normalize or ignore meta.generatedAt to avoid flakiness. If it’s intentionally fixed for fixtures, ignore this note.

You can confirm by checking the test harness comparator or snapshot logic.

packages/vue-to-dsl/package.json (2)

12-14: Ensure CLI binary is executable and has a shebang; confirm tsconfig.cli.json outputs ESM/CJS consistently

dist/cli.js must start with a shebang and be executable in published tarball. Also confirm the CLI build target matches "type":"module" (ESM) or switch to .cjs consistently.

If needed, add a postbuild to inject the shebang:

   "scripts": {
-    "build": "pnpm run build:types && vite build && pnpm run build:cli",
+    "build": "pnpm run build:types && vite build && pnpm run build:cli && node ./scripts/add-shebang.mjs",

And create scripts/add-shebang.mjs:

import { readFileSync, writeFileSync } from 'node:fs'
const p = 'packages/vue-to-dsl/dist/cli.js'
const s = readFileSync(p, 'utf8')
if (!s.startsWith('#!')) writeFileSync(p, `#!/usr/bin/env node\n${s}`)

Alternatively, set "tsconfig.cli.json" to preserve shebangs.

Also applies to: 18-26


38-45: Double-check dependency classification for runtime vs. build-only

If @vue/compiler-sfc and @vue/compiler-dom are only used during conversion at runtime (library/CLI), they belong in dependencies (OK). If they’re only used at build time, move them to devDependencies. Also consider whether depending on "vue" at runtime is necessary for parsing—often the compilers suffice.

Would you like a quick audit script to scan src for runtime imports and propose a move?

packages/vue-to-dsl/README.md (2)

218-233: Fix markdownlint: add language for fenced code block; also align file extensions with TypeScript sources

The “项目结构” block misses a language identifier and lists .js while the package builds from TS.

Apply this diff:

-```
+```text
 src/
-├── converter.js          # 主转换器类
-├── parser/               # SFC解析器
-│   └── index.js
-├── parsers/              # 各部分解析器
-│   ├── templateParser.js # 模板解析器
-│   ├── scriptParser.js   # 脚本解析器
-│   ├── styleParser.js    # 样式解析器
-│   └── index.js
-├── generator/            # Schema生成器
-│   └── index.js
-├── types/                # 类型定义
-│   └── index.js
-└── index.js              # 主入口文件
+├── converter.ts          # 主转换器类
+├── parser/               # SFC解析器
+│   └── index.ts
+├── parsers/              # 各部分解析器
+│   ├── templateParser.ts # 模板解析器
+│   ├── scriptParser.ts   # 脚本解析器
+│   ├── styleParser.ts    # 样式解析器
+│   └── index.ts
+├── generator/            # Schema生成器
+│   └── index.ts
+├── types/                # 类型定义(如有 JS 运行时代码则保持 .js)
+│   └── index.ts
+└── index.ts              # 主入口文件

29-67: Top-level await in the example may confuse users outside ESM—wrap or note requirement

The sample uses top-level await. Either declare it requires Node ESM (“type”: “module”) or wrap in an async IIFE.

Example alternative:

-const converter = new VueToDslConverter()
-// 从字符串转换
-const vueCode = `
+const converter = new VueToDslConverter()
+;(async () => {
+  // 从字符串转换
+  const vueCode = `
   ...SFC...
-const result = await converter.convertFromString(vueCode)
-if (result.schema) {
-  console.log('转换成功:', result.schema)
-} else {
-  console.error('转换失败:', result.errors)
-}
+  const result = await converter.convertFromString(vueCode)
+  if (result.schema) console.log('转换成功:', result.schema)
+  else console.error('转换失败:', result.errors)
+})()
packages/vue-to-dsl/test/testcases/005_createVM/input/component.vue (3)

5-9: Bind numeric prop instead of string literal for active

active looks numeric; bind it as a number to avoid type coercion.

-        active="2"
+        :active="2"

406-411: Scoped style targeting body will not apply as expected

Scoped styles are attribute-scoped to the component root; body won’t match. Use :global or drop scoped for this rule.

-<style scoped>
-body {
-  background-color: #eef0f5;
-  margin-bottom: 80px;
-}
-</style>
+<style scoped>
+:global(body) {
+  background-color: #eef0f5;
+  margin-bottom: 80px;
+}
+</style>

82-90: Repeat of conflicting form props across the file

Same label-position/label-width duplication appears in several tiny-form blocks. Please normalize as in the earlier suggestion to avoid ambiguity.

Also applies to: 196-204, 254-262, 330-337

packages/vue-to-dsl/src/parsers/styleParser.ts (2)

1-6: Type the public API and harden parsing (skip @-rules in parseCSSRules, strip comments, safer matches)

Current functions return any and use regexes that can over-match. Type the outputs, ignore @-rules in parseCSSRules (media is handled separately), and strip CSS comments before parsing. Backwards compatible.

-export function parseStyle(style: string, options: any = {}) {
+export interface StyleParseOptions {
+  scoped?: boolean
+  lang?: string
+}
+
+export interface CSSRule {
+  selector: string
+  declarations: Record<string, string>
+}
+
+export interface MediaQueryBlock {
+  condition: string
+  content: string
+  rules: CSSRule[]
+}
+
+export function parseStyle(style: string, options: StyleParseOptions = {}) {
   if (!style || !style.trim()) {
     return { css: '', scoped: false, lang: 'css' }
   }
   return { css: style.trim(), scoped: options.scoped || false, lang: options.lang || 'css' }
 }
 
-function parseDeclarations(declarations: string) {
+function stripComments(input: string) {
+  return input.replace(/\/\*[\s\S]*?\*\//g, '')
+}
+
+function parseDeclarations(declarations: string) {
   const result: Record<string, string> = {}
   if (!declarations) return result
-  const declarationList = declarations.split(';').filter((d) => d.trim())
+  const declarationList = declarations.split(';').filter((d) => d.trim())
   declarationList.forEach((declaration) => {
     const colonIndex = declaration.indexOf(':')
     if (colonIndex > 0) {
       const property = declaration.substring(0, colonIndex).trim()
       const value = declaration.substring(colonIndex + 1).trim()
       result[property] = value
     }
   })
   return result
 }
 
-export function parseCSSRules(css: string) {
-  if (!css || !css.trim()) return [] as any[]
-  const rules: any[] = []
-  const ruleMatches = css.match(/([^{}]+)\s*{([^{}]*)}/g)
+export function parseCSSRules(css: string): CSSRule[] {
+  if (!css || !css.trim()) return []
+  const rules: CSSRule[] = []
+  const cleaned = stripComments(css)
+  // skip at-rules (@media handled via extractMediaQueries)
+  const ruleMatches = cleaned.match(/(?:(?!@)[^{}]+)\s*{([^{}]*)}/g)
   if (ruleMatches) {
     ruleMatches.forEach((ruleMatch) => {
-      const match = ruleMatch.match(/([^{}]+)\s*{([^{}]*)}/)
+      const match = ruleMatch.match(/([^{}]+)\s*{([^{}]*)}/)
       if (match) {
         const selector = match[1].trim()
         const declarations = match[2].trim()
         rules.push({ selector, declarations: parseDeclarations(declarations) })
       }
     })
   }
   return rules
 }
 
-export function extractCSSVariables(css: string) {
+export function extractCSSVariables(css: string): Record<string, string> {
   const variables: Record<string, string> = {}
   if (!css) return variables
-  const variableMatches = css.match(/--[\w-]+\s*:\s*[^;]+/g)
+  const variableMatches = stripComments(css).match(/--[\w-]+\s*:\s*[^;]+/g)
   if (variableMatches) {
     variableMatches.forEach((match) => {
       const colonIndex = match.indexOf(':')
       if (colonIndex > 0) {
         const name = match.substring(0, colonIndex).trim()
         const value = match.substring(colonIndex + 1).trim()
         variables[name] = value
       }
     })
   }
   return variables
 }
 
-export function hasMediaQueries(css: string) {
+export function hasMediaQueries(css: string): boolean {
   if (!css) return false
   return /@media\s+/.test(css)
 }
 
-export function extractMediaQueries(css: string) {
-  const mediaQueries: any[] = []
+export function extractMediaQueries(css: string): MediaQueryBlock[] {
+  const mediaQueries: MediaQueryBlock[] = []
   if (!css) return mediaQueries
-  const mediaMatches = css.match(/@media[^{]+\{[^{}]*(?:\{[^{}]*\}[^{}]*)*\}/g)
+  const mediaMatches = stripComments(css).match(/@media[^{]+\{[^{}]*(?:\{[^{}]*\}[^{}]*)*\}/g)
   if (mediaMatches) {
     mediaMatches.forEach((match) => {
       const conditionMatch = match.match(/@media\s+([^{]+)/)
       if (conditionMatch) {
         const condition = conditionMatch[1].trim()
         const content = match.substring(match.indexOf('{') + 1, match.lastIndexOf('}')).trim()
         mediaQueries.push({ condition, content, rules: parseCSSRules(content) })
       }
     })
   }
   return mediaQueries
 }

Also applies to: 23-38, 40-55, 57-61, 62-77


23-38: Add tests for comment stripping and nested rules inside @media

Current regexes can be tripped by comments and nested content. Please add unit tests covering:

  • CSS with comments
  • @media with multiple nested rules
  • Variables inside @media blocks

I can scaffold vitest specs under packages/vue-to-dsl/test/styleParser.test.ts if you want.

Also applies to: 62-77

packages/vue-to-dsl/test/testcases/002_complex/expected/schema.json (1)

255-263: Undefined identifiers in expressions: canEdit (missing .value?) and showStats (not declared)

  • The button disabled uses "!canEdit". Depending on DSL runtime, ensure this resolves to the computed value. If it needs ".value" or "state.", adjust.
  • "condition": "showStats" references an undeclared symbol. Consider "state.showStats" and include showStats in state.

If adopting state.showStats:

-  "condition": "showStats",
+  "condition": "state.showStats",

And in state:

   "state": {
+    "showStats": true,

Also applies to: 360-363

packages/vue-to-dsl/src/types/index.js (3)

14-19: Tighten typedefs for options to prevent misuse downstream

Suggest making shapes explicit so editors and TS inference are more helpful (especially for componentMap and customParsers).

Apply:

- * @typedef {Object} ConvertOptions
- * @property {Object} componentMap - 组件映射配置
+ * @typedef {Object} ConvertOptions
+ * @property {Record<string, string>} componentMap - 组件映射配置
  * @property {boolean} preserveComments - 是否保留注释
  * @property {boolean} strictMode - 是否严格模式
- * @property {Object} customParsers - 自定义解析器
+ * @property {{ template?: { parse: Function }, script?: { parse: Function }, style?: { parse: Function } }} customParsers - 自定义解析器

29-42: Clarify collection element types in PageSchema

props and children as raw Array lose intent; align them with expected shapes (even if loosely).

Apply:

- * @property {Array} props - 属性
+ * @property {Array<Object>} props - 属性
  * @property {string} css - 样式
- * @property {Array} children - 子组件
+ * @property {Array<TemplateSchema>} children - 子组件

45-53: Surface special fields in TemplateSchema

Schema defines key and ref as top-level, but current typedef leaves them as generic. Make this explicit to guide parsers/generators.

Apply:

- * @property {string} key - 唯一键
- * @property {string} ref - 引用
+ * @property {string} [key] - 唯一键
+ * @property {string} [ref] - 引用
packages/vue-to-dsl/test/testcases/002_complex/input/component.vue (2)

73-75: Fetch robustness: handle HTTP errors before parsing JSON

fetch only throws on network errors. Non-2xx responses should be checked to avoid await response.json() on error pages.

Apply:

-        const response = await fetch(`/api/users/${props.userId}`)
-        const data = await response.json()
+        const response = await fetch(`/api/users/${props.userId}`)
+        if (!response.ok) throw new Error(`HTTP ${response.status}`)
+        const data = await response.json()

99-99: Empty onUnmounted block can be dropped

No cleanup logic; remove for clarity.

Apply:

-    onUnmounted(() => {})
+    // onUnmounted: 无需清理
packages/vue-to-dsl/src/parsers/templateParser.ts (2)

42-50: Hoist special attributes key and ref to top-level fields

parseNodeProps currently places all attributes into schema.props. Your types define top-level key and ref; migrate them out when present.

Apply, within parseTemplateNode after parsing props:

-  if (node.props && node.props.length > 0) schema.props = parseNodeProps(node.props, options)
+  if (node.props && node.props.length > 0) {
+    schema.props = parseNodeProps(node.props, options)
+    if (schema.props.key != null) {
+      schema.key = String(schema.props.key)
+      delete schema.props.key
+    }
+    if (schema.props.ref != null) {
+      schema.ref = String(schema.props.ref)
+      delete schema.props.ref
+    }
+  }

6-39: Micro: move HTML tag list to a Set and hoist out of function

Saves allocations per call and gives O(1) lookups.

Apply:

-  const htmlTags = [
+  const htmlTags = new Set([
     'div',
     ...
-    'main'
-  ]
-  if (htmlTags.includes(tag.toLowerCase())) return tag.toLowerCase()
+    'main'
+  ])
+  if (htmlTags.has(tag.toLowerCase())) return tag.toLowerCase()

And hoist htmlTags to module scope.

packages/vue-to-dsl/test/testcases/index.test.js (3)

26-26: Directory discovery pattern can false-positive

/\d+_/ matches anywhere in the name. If you only want directories starting with digits + underscore (e.g., 001_simple), anchor the regex.

Apply:

-  const cases = fs.readdirSync(baseDir).filter((name) => /\d+_/.test(name))
+  const cases = fs.readdirSync(baseDir).filter((name) => /^\d+_/.test(name))

37-41: Avoid unconditional writes in tests

Writing artifacts during tests can pollute worktrees/CI. Gate behind an env var or use test.onTestFinished hooks.

Apply:

-      fs.mkdirSync(path.dirname(outputFile), { recursive: true })
-      fs.writeFileSync(outputFile, JSON.stringify(result.schema, null, 2))
+      if (process.env.UPDATE_OUTPUT === '1') {
+        fs.mkdirSync(path.dirname(outputFile), { recursive: true })
+        fs.writeFileSync(outputFile, JSON.stringify(result.schema, null, 2))
+      }

47-73: Strengthen assertions to catch structural regressions

Current checks only assert presence of keys. Consider deep equality (allowing additional keys) to ensure converter fidelity.

Example enhancement:

  • Compare expected.methods[m] and typeof result.schema.methods[m] === 'object'
  • Optionally expect(result.schema).toMatchObject(expected) for flexible shape matching.

If you prefer strict conformance for some cases, add a strict: true flag in each fixture to opt-in to toStrictEqual.

packages/vue-to-dsl/src/index.ts (2)

1-1: Avoid importing .d.ts for side effects; let the TS compiler include types

import './index.d.ts' is unnecessary and can confuse bundlers (there’s no runtime output for .d.ts). Remove it and expose typings via the package.json types field or by emitting .d.ts from build.

-import './index.d.ts'

3-7: Public API looks good; consider type-only exports for clarity

Re-exports are fine. If you intend consumers to import types, consider explicit type-only exports to avoid accidental value re-exports and improve treeshaking clarity. No blocker.

packages/vue-to-dsl/test/sfc/converter.test.js (2)

4-6: Solid e2e happy-path; add an assertion for generated ids

Since the generator assigns 8-char ids to all component nodes (including the root Page), add a quick assertion so we don’t regress id assignment.

     expect(result.schema.css).toBeDefined()
     expect(result.schema.children).toBeDefined()
+    // id assignment
+    expect(typeof result.schema.id).toBe('string')
+    expect(result.schema.id.length).toBe(8)
   })

Also applies to: 34-43


82-104: Add a test for non-state reactive/ref variables to prevent blind spots

Current parser primarily extracts reactive when the variable is named state. A common pattern is const count = ref(1). Add a test now; we’ll align the parser shortly (see parser comment).

   it('should parse <script setup> reactive/computed/lifecycle', async () => {
@@
     expect(result.schema.methods.inc).toBeDefined()
   })
+
+  it('should collect standalone refs in <script setup>', async () => {
+    const vueCode = `
+<template><div>{{ count }}</div></template>
+<script setup>
+  import { ref } from 'vue'
+  const count = ref(1)
+</script>`
+    const result = await converter.convertFromString(vueCode)
+    expect(result.errors).toHaveLength(0)
+    expect(result.schema.state.count).toBe(1)
+  })
packages/vue-to-dsl/cli.ts (2)

12-13: Import from the package entry (index) to avoid deep path coupling

Importing the converter through ./src/converter ties the CLI to internal layout. Prefer the public entry (which re-exports the converter) so built artifacts remain stable across refactors.

-import { VueToDslConverter } from './src/converter'
+import { VueToDslConverter } from './src' // resolved to public index during build

142-156: Export format “js” is ESM-only; consider adding cjs or naming it “esm”

If consumers expect CommonJS, this will surprise them. Either add --format cjs or rename to esm and document. Optional for this PR.

packages/vue-to-dsl/src/generator/index.ts (3)

101-115: Consider deterministic ids for repeatable outputs (seed option)

Math.random() ids make diffs noisy. Add an optional seed in options or a deterministicId(startAt=0) mode for stable conversion in CI. Optional but valuable for fixtures.


108-115: ID assignment only walks children; extend to cover common child carriers (e.g., slots)

If your template schema nests components under slots, slotChildren, or similar, ids won’t be assigned there. Consider iterating all object-valued props and arrays that contain nodes with componentName.


20-40: Value conversion: support template literals and bigint numerics

convertToPlainValue doesn’t handle template literals or bigint (e.g., 123n). Not a blocker, but easy to extend if needed.

packages/vue-to-dsl/src/parsers/scriptParser.ts (3)

300-334: Options API data() is stubbed; parse returned object for initial state

Currently data becomes { data: 'function() { return {} }' } which is not useful downstream. Parse the returned object literal to populate result.state. This will unlock parity with Setup API.


47-88: getNodeValue returns the string 'undefined' in some cases; return actual undefined

Returning 'undefined' as a string can leak into schema and cause confusing output. Prefer the real undefined.

-    return 'undefined'
+    return undefined
@@
-  return 'undefined'
+  return undefined

359-389: Lifecycle detection is good; also consider member calls e.g., Vue.onMounted

You already support member calls in isVueReactiveCall; applying a similar pattern for lifecycle (identifier vs member) would improve robustness in atypical codebases.

packages/vue-to-dsl/src/converter.ts (4)

49-54: Propagate SFC parser warnings instead of dropping them.

parseSFC currently logs warnings to console; these never make it into warnings. Capture and surface them to callers.

   try {
-      const sfcResult = parseSFC(vueCode)
+      const sfcResult = parseSFC(vueCode)
+      if ((sfcResult as any).warnings?.length) {
+        warnings.push(...(sfcResult as any).warnings)
+      }
       if (!sfcResult.template && !sfcResult.scriptSetup && !sfcResult.script) {
         throw new Error('Invalid Vue SFC: no template or script found')
       }

80-83: Consider filtering out type-only or local self-imports when collecting dependencies.

If parseScript includes type-only imports or self-referential paths, bubbling them up may over-report dependencies. Optional enhancement: filter by !imp.isTypeOnly and exclude relative sources as needed before de-duplication.


128-131: Populate schema.path derived from filename when absent.

Many downstream tools expect a route-like path. You already inject fileName and meta.name; add a default path once.

       if (result.schema) {
         result.schema.fileName = fileName
         result.schema.meta.name = fileName
+        if (!result.schema.path) {
+          result.schema.path = `/${fileName}`
+        }
       }

138-154: Speed up multi-file conversion and preserve per-file attribution.

Use Promise.all for parallelism and attach the filePath for easier diagnostics.

-  async convertMultipleFiles(filePaths: string[]): Promise<ConvertResult[]> {
-    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
-  }
+  async convertMultipleFiles(filePaths: string[]): Promise<ConvertResult[]> {
+    const tasks = filePaths.map(async (filePath) => {
+      try {
+        const result = await this.convertFromFile(filePath)
+        // Optional: tag result for caller-side attribution
+        ;(result as any).filePath = filePath
+        return result
+      } catch (error: any) {
+        return {
+          schema: null,
+          dependencies: [],
+          errors: [`Failed to convert ${filePath}: ${error.message}`],
+          warnings: [],
+          filePath
+        } as any
+      }
+    })
+    return Promise.all(tasks)
+  }
packages/vue-to-dsl/src/parser/index.ts (1)

43-46: Harden file parsing with context-rich errors.

Wrap IO in try/catch and rethrow with filePath context so upstream can show actionable errors.

-export async function parseVueFile(filePath: string): Promise<any> {
-  const content = await fs.readFile(filePath, 'utf-8')
-  return parseSFC(content)
-}
+export async function parseVueFile(filePath: string): Promise<any> {
+  try {
+    const content = await fs.readFile(filePath, 'utf-8')
+    return parseSFC(content)
+  } catch (e: any) {
+    const err = new Error(`Failed to read Vue file "${filePath}": ${e.message}`)
+    ;(err as any).cause = e
+    throw err
+  }
+}
packages/vue-to-dsl/src/index.d.ts (1)

29-38: Include id assigned by generator in TemplateSchema.

IDs are assigned during generation. Expose them in the type.

   export interface TemplateSchema {
     componentName: string
+    id?: string
     props?: Record<string, any>
     children?: TemplateSchema[]
     condition?: string
     loop?: string
     key?: string
     ref?: string
     [key: string]: any
   }
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 108919d and 3ce30b1.

📒 Files selected for processing (28)
  • packages/vue-to-dsl/.gitignore (1 hunks)
  • packages/vue-to-dsl/README.md (1 hunks)
  • packages/vue-to-dsl/cli.ts (1 hunks)
  • packages/vue-to-dsl/package.json (1 hunks)
  • packages/vue-to-dsl/src/converter.ts (1 hunks)
  • packages/vue-to-dsl/src/generator/index.ts (1 hunks)
  • packages/vue-to-dsl/src/index.d.ts (1 hunks)
  • packages/vue-to-dsl/src/index.ts (1 hunks)
  • packages/vue-to-dsl/src/parser/index.ts (1 hunks)
  • packages/vue-to-dsl/src/parsers/index.ts (1 hunks)
  • packages/vue-to-dsl/src/parsers/scriptParser.ts (1 hunks)
  • packages/vue-to-dsl/src/parsers/styleParser.ts (1 hunks)
  • packages/vue-to-dsl/src/parsers/templateParser.ts (1 hunks)
  • packages/vue-to-dsl/src/types/index.js (1 hunks)
  • packages/vue-to-dsl/test/sfc/converter.test.js (1 hunks)
  • packages/vue-to-dsl/test/testcases/001_simple/expected/schema.json (1 hunks)
  • packages/vue-to-dsl/test/testcases/001_simple/input/component.vue (1 hunks)
  • packages/vue-to-dsl/test/testcases/002_complex/expected/schema.json (1 hunks)
  • packages/vue-to-dsl/test/testcases/002_complex/input/component.vue (1 hunks)
  • packages/vue-to-dsl/test/testcases/003_options_api/expected/schema.json (1 hunks)
  • packages/vue-to-dsl/test/testcases/003_options_api/input/component.vue (1 hunks)
  • packages/vue-to-dsl/test/testcases/004_setup_api/expected/schema.json (1 hunks)
  • packages/vue-to-dsl/test/testcases/004_setup_api/input/component.vue (1 hunks)
  • packages/vue-to-dsl/test/testcases/005_createVM/expected/schema.json (1 hunks)
  • packages/vue-to-dsl/test/testcases/005_createVM/input/component.vue (1 hunks)
  • packages/vue-to-dsl/test/testcases/index.test.js (1 hunks)
  • packages/vue-to-dsl/tsconfig.json (1 hunks)
  • packages/vue-to-dsl/vite.config.js (1 hunks)
🧰 Additional context used
🧠 Learnings (5)
📓 Common learnings
Learnt from: chilingling
PR: opentiny/tiny-engine#837
File: packages/vue-generator/src/plugins/genDependenciesPlugin.js:66-66
Timestamp: 2024-09-30T07:51:10.036Z
Learning: In the `tiny-engine` project, `opentiny/tiny-engine-dsl-vue` refers to the current package itself, and importing types from it may cause circular dependencies.
📚 Learning: 2024-09-30T07:51:10.036Z
Learnt from: chilingling
PR: opentiny/tiny-engine#837
File: packages/vue-generator/src/plugins/genDependenciesPlugin.js:66-66
Timestamp: 2024-09-30T07:51:10.036Z
Learning: In the `tiny-engine` project, `opentiny/tiny-engine-dsl-vue` refers to the current package itself, and importing types from it may cause circular dependencies.

Applied to files:

  • packages/vue-to-dsl/README.md
  • packages/vue-to-dsl/vite.config.js
  • packages/vue-to-dsl/cli.ts
  • packages/vue-to-dsl/src/index.d.ts
  • packages/vue-to-dsl/package.json
📚 Learning: 2025-01-14T08:42:18.574Z
Learnt from: gene9831
PR: opentiny/tiny-engine#1038
File: packages/plugins/block/index.js:24-24
Timestamp: 2025-01-14T08:42:18.574Z
Learning: In the tiny-engine project, breaking changes are documented in the changelog rather than in JSDoc comments or separate migration guides.

Applied to files:

  • packages/vue-to-dsl/README.md
📚 Learning: 2024-10-09T01:47:35.507Z
Learnt from: chilingling
PR: opentiny/tiny-engine#583
File: packages/build/vite-config/index.js:1-1
Timestamp: 2024-10-09T01:47:35.507Z
Learning: The `getDefaultConfig` function inside `packages/build/vite-config/src/default-config.js` is intended to remain and be called internally. Ensure no references to this function exist outside of this file.

Applied to files:

  • packages/vue-to-dsl/vite.config.js
📚 Learning: 2025-01-14T06:40:18.223Z
Learnt from: gene9831
PR: opentiny/tiny-engine#1011
File: packages/plugins/page/src/PageGeneral.vue:166-178
Timestamp: 2025-01-14T06:40:18.223Z
Learning: The page structure data in PageGeneral.vue cannot have circular dependencies due to design constraints.

Applied to files:

  • packages/vue-to-dsl/test/testcases/003_options_api/expected/schema.json
🧬 Code graph analysis (9)
packages/vue-to-dsl/src/parsers/scriptParser.ts (2)
packages/vue-to-dsl/src/index.ts (1)
  • parseScript (6-6)
packages/vue-to-dsl/src/parsers/index.ts (1)
  • parseScript (2-2)
packages/vue-to-dsl/test/testcases/index.test.js (1)
packages/vue-to-dsl/src/converter.ts (1)
  • VueToDslConverter (31-163)
packages/vue-to-dsl/src/parsers/styleParser.ts (2)
packages/vue-to-dsl/src/index.ts (1)
  • parseStyle (6-6)
packages/vue-to-dsl/src/parsers/index.ts (5)
  • parseStyle (3-3)
  • parseCSSRules (3-3)
  • extractCSSVariables (3-3)
  • hasMediaQueries (3-3)
  • extractMediaQueries (3-3)
packages/vue-to-dsl/src/parsers/templateParser.ts (3)
packages/vue-generator/src/generator/vue/sfc/generateAttribute.js (1)
  • props (596-596)
packages/vue-to-dsl/src/index.ts (1)
  • parseTemplate (6-6)
packages/vue-to-dsl/src/parsers/index.ts (1)
  • parseTemplate (1-1)
packages/vue-to-dsl/src/generator/index.ts (3)
packages/vue-generator/src/generator/vue/sfc/generateAttribute.js (1)
  • props (596-596)
packages/vue-generator/src/templates/vue-template/index.js (1)
  • value (30-30)
packages/vue-to-dsl/src/index.ts (2)
  • generateSchema (5-5)
  • generateAppSchema (5-5)
packages/vue-to-dsl/test/sfc/converter.test.js (2)
packages/vue-to-dsl/test/testcases/index.test.js (1)
  • converter (8-24)
packages/vue-to-dsl/src/converter.ts (1)
  • VueToDslConverter (31-163)
packages/vue-to-dsl/cli.ts (1)
packages/vue-to-dsl/src/converter.ts (1)
  • VueToDslConverter (31-163)
packages/vue-to-dsl/src/converter.ts (5)
packages/vue-to-dsl/src/parser/index.ts (1)
  • parseSFC (4-41)
packages/vue-to-dsl/src/parsers/templateParser.ts (1)
  • parseTemplate (231-238)
packages/vue-to-dsl/src/parsers/scriptParser.ts (1)
  • parseScript (399-427)
packages/vue-to-dsl/src/parsers/styleParser.ts (1)
  • parseStyle (1-6)
packages/vue-to-dsl/src/generator/index.ts (1)
  • generateSchema (140-165)
packages/vue-to-dsl/src/parser/index.ts (2)
packages/build/vite-plugin-meta-comments/src/transform-sfc.js (1)
  • parse (50-50)
packages/vue-generator/src/utils/vue-sfc-validator.js (1)
  • errors (26-26)
🪛 markdownlint-cli2 (0.17.2)
packages/vue-to-dsl/README.md

218-218: Fenced code blocks should have a language specified

(MD040, fenced-code-language)

🪛 Biome (2.1.2)
packages/vue-to-dsl/src/parsers/templateParser.ts

[error] 133-133: The regular expression includes this negated empty character class.

Negated empty character classes match anything.
If you want to match against [, escape it [.
Otherwise, remove the character class or fill it.

(lint/correctness/noEmptyCharacterClassInRegex)


[error] 133-133: The regular expression includes this negated empty character class.

Negated empty character classes match anything.
If you want to match against [, escape it [.
Otherwise, remove the character class or fill it.

(lint/correctness/noEmptyCharacterClassInRegex)


[error] 133-133: The regular expression includes this negated empty character class.

Negated empty character classes match anything.
If you want to match against [, escape it [.
Otherwise, remove the character class or fill it.

(lint/correctness/noEmptyCharacterClassInRegex)


[error] 133-133: The regular expression includes this negated empty character class.

Negated empty character classes match anything.
If you want to match against [, escape it [.
Otherwise, remove the character class or fill it.

(lint/correctness/noEmptyCharacterClassInRegex)

🔇 Additional comments (17)
packages/vue-to-dsl/tsconfig.json (2)

5-5: Confirmed: TypeScript ≥ 5.2 in use, so moduleResolution: "Bundler" is supported

Both the root and the vue-to-dsl package declare TS versions well above 5.2, so no changes are needed.

• Root package.json: "typescript": "~5.4.5"
packages/vue-to-dsl/package.json: "typescript": "^5.4.0"


8-9: Declarations emission verified
The build script in packages/vue-to-dsl/package.json runs tsc -p tsconfig.json (via build:types) to emit .d.ts files into dist, and then invokes vite build to produce the JavaScript bundles. With declaration: true and emitDeclarationOnly: true in tsconfig, you will end up with both JS and type declarations in dist as intended—no additional steps are required.

Optional refactor:

  • To simplify the pipeline, you could add vite-plugin-dts to your Vite configuration and remove the separate tsc invocation, ensuring declarations are generated during the Vite build.
packages/vue-to-dsl/test/testcases/004_setup_api/input/component.vue (1)

11-33: Good, representative script-setup fixture.

Covers refs/reactive/computed, function + arrow function (exercises your “arrow → named” normalization), and lifecycle hooks—useful breadth for the converter.

packages/vue-to-dsl/test/testcases/005_createVM/expected/schema.json (3)

31-31: Type fidelity: numeric props serialized as strings.

"active": "2" is likely numeric. Confirm the DSL spec; if numeric, serialize as number to avoid runtime coercion.

-                "active": "2",
+                "active": 2,

836-839: Loop expression looks correct.

Using a JSExpression to iterate over state aligns with the DSL’s dynamic data patterns.


1-20: Meta.generatedAt is static in fixture—ensure tests remain stable.

If your tests compare full JSON, a static timestamp is fine. If you switch to dynamic timestamps, exclude or normalize this field during assertions.

packages/vue-to-dsl/test/testcases/003_options_api/input/component.vue (1)

24-32: Mixing Options API with setup(): OK

Using data()/methods/mounted alongside setup() is valid in Vue 3 and is a good fixture to exercise the converter. No issues here.

packages/vue-to-dsl/test/testcases/004_setup_api/expected/schema.json (1)

14-47: Rest looks coherent

Methods, computed, and lifecycle shapes align with the described converter behavior. IDs and CSS are consistent with other fixtures.

packages/vue-to-dsl/test/testcases/001_simple/input/component.vue (1)

1-8: Template and styles: LGTM

The structure and scoped styles are clear and good for exercising basic bindings and events.

Also applies to: 42-61

packages/vue-to-dsl/test/testcases/001_simple/expected/schema.json (1)

14-25: Align expected method bodies with ref-based count

If you adopt the ref fix in the input, update the schema’s method bodies to reference count.value rather than state.count.value.

   "methods": {
     "increment": {
       "type": "JSFunction",
-      "value": "function increment() { state.count.value++ }"
+      "value": "function increment() { count.value++ }"
     },
     "decrement": {
       "type": "JSFunction",
-      "value": "function decrement() { state.count.value-- }"
+      "value": "function decrement() { count.value-- }"
     },
     "reset": {
       "type": "JSFunction",
-      "value": "function reset() { state.count.value = 0 }"
+      "value": "function reset() { count.value = 0 }"
     }
   },
packages/vue-to-dsl/src/types/index.js (1)

73-73: Module boundary looks good

Using export {} to force module scope is correct and avoids global JSDoc bleed.

packages/vue-to-dsl/src/parsers/templateParser.ts (1)

151-165: v-on without argument is not handled (object syntax)

v-on="listenersObject" is valid but this branch assumes a single event name, defaulting to 'click'. Consider supporting the object form or fail clearly.

I can extend this to detect prop.arg === null and attempt literal/object parsing, mapping each key to a JSExpression. Confirm if your DSL expects spread-like semantics or materialized props per event.

packages/vue-to-dsl/src/parsers/index.ts (1)

1-3: Public surface looks coherent

Re-exporting the three parsers (and style utilities) from one place is clean and keeps the main entry lightweight.

packages/vue-to-dsl/cli.ts (1)

158-166: Ensure output directory creation covers absolute paths and Windows drive roots

Current logic is fine; just a note that path.dirname('C:\\a.js') yields 'C:\\' which is truthy, so mkdir is attempted (OK). No action.

packages/vue-to-dsl/src/converter.ts (1)

156-162: Options mutator/getter look good.

packages/vue-to-dsl/src/parser/index.ts (1)

48-50: Validation helper is concise and correct.

packages/vue-to-dsl/src/index.d.ts (1)

1-154: No circular self-imports in source files; only in README.md examples

The import scan under packages/vue-to-dsl/ found no instances of

import  from '@opentiny/tiny-engine-vue-to-dsl'

in any source files—only in the documentation example at:

  • packages/vue-to-dsl/README.md:29

Since this is purely a usage example and not part of the package’s implementation, it poses no risk of circular dependency. No code changes are required.

Comment on lines +44 to +66
for (let i = 1; i < args.length; i += 2) {
const option = args[i]
const value = args[i + 1]

switch (option) {
case '--output':
case '-o':
outputFile = value
break
case '--format':
case '-f':
if (value === 'json' || value === 'js') {
format = value
}
break
case '--help':
case '-h':
console.log('显示帮助信息...')
process.exit(0)
break
}
}

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Verification agent

🧩 Analysis chain

Make option parsing robust (handle flags without values and mixed order)

The current “step by 2” iteration breaks for flags at the end with missing values and ignores mixed ordering. A small parser tweak keeps it dependency-free while resilient.

-for (let i = 1; i < args.length; i += 2) {
-  const option = args[i]
-  const value = args[i + 1]
-
-  switch (option) {
+for (let i = 1; i < args.length; i++) {
+  const option = args[i]
+  const next = args[i + 1]
+  const hasValue = next && !next.startsWith('-')
+  switch (option) {
     case '--output':
     case '-o':
-      outputFile = value
-      break
+      if (!hasValue) {
+        console.error('❌ 错误: --output/-o 需要一个文件路径')
+        process.exit(1)
+      }
+      outputFile = next
+      i++
+      break
     case '--format':
     case '-f':
-      if (value === 'json' || value === 'js') {
-        format = value
-      }
-      break
+      if (!hasValue) {
+        console.error('❌ 错误: --format/-f 需要一个值: json | js')
+        process.exit(1)
+      }
+      if (next === 'json' || next === 'js') {
+        format = next as any
+      } else {
+        console.error(`❌ 错误: 不支持的输出格式 - ${next}`)
+        process.exit(1)
+      }
+      i++
+      break
     case '--help':
     case '-h':
       console.log('显示帮助信息...')
       process.exit(0)
       break
+    default:
+      if (option.startsWith('-')) {
+        console.warn(`⚠️ 未知参数: ${option}(已忽略)`)
+      }
   }
 }

Run the CLI locally with edge cases:

  • With trailing “-o” and no value (should error).
  • Mixed order: “file.vue -f js -o out.js”.
  • Unknown flag: “--foo” (should warn).

🏁 Script executed:

#!/bin/bash
# Locate the CLI file
CLI_FILE=$(fd cli.ts)
echo "Found CLI file at: $CLI_FILE"

# Print the first 100 lines to review the argument parsing logic
sed -n '1,100p' "$CLI_FILE"

Length of output: 2740


Improve CLI argument parsing robustness

The current loop in packages/vue-to-dsl/cli.ts (lines 44–66) steps by two and assumes every flag has a following value. This silently misbehaves when a flag is trailing with no value, accepts invalid formats without error, and ignores unknown flags entirely. We should:

  • Iterate over args one-by-one, detect whether the next token is a value (non-flag) before consuming it
  • Emit errors and process.exit(1) for missing or invalid values on --output/-o and --format/-f
  • Warn (but continue) on any unknown flags
  • Increment the index when a flag’s value is consumed

Proposed diff at lines 44–66 in packages/vue-to-dsl/cli.ts:

-for (let i = 1; i < args.length; i += 2) {
-  const option = args[i]
-  const value = args[i + 1]
-
-  switch (option) {
+for (let i = 1; i < args.length; i++) {
+  const option = args[i]
+  const next   = args[i + 1]
+  // value exists and isn’t another flag
+  const hasValue = next !== undefined && !next.startsWith('-')
+  switch (option) {
     case '--output':
     case '-o':
-      outputFile = value
-      break
+      if (!hasValue) {
+        console.error('❌ 错误: --output/-o 需要一个文件路径')
+        process.exit(1)
+      }
+      outputFile = next
+      i++
+      break
     case '--format':
     case '-f':
-      if (value === 'json' || value === 'js') {
-        format = value
-      }
-      break
+      if (!hasValue) {
+        console.error('❌ 错误: --format/-f 需要一个值: json | js')
+        process.exit(1)
+      }
+      if (next === 'json' || next === 'js') {
+        format = next
+      } else {
+        console.error(`❌ 错误: 不支持的输出格式 - ${next}`)
+        process.exit(1)
+      }
+      i++
+      break
     case '--help':
     case '-h':
       console.log('显示帮助信息...')
       process.exit(0)
       break
+    default:
+      if (option.startsWith('-')) {
+        console.warn(`⚠️ 未知参数: ${option}(已忽略)`)
+      }
   }
 }

Run these edge-case checks after applying:

  • Trailing flag without value: node cli.ts file.vue -o (should exit with error)
  • Mixed ordering: node cli.ts file.vue -f js -o out.js (both flags applied)
  • Unknown flag: node cli.ts file.vue --foo (warns, continues)
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
for (let i = 1; i < args.length; i += 2) {
const option = args[i]
const value = args[i + 1]
switch (option) {
case '--output':
case '-o':
outputFile = value
break
case '--format':
case '-f':
if (value === 'json' || value === 'js') {
format = value
}
break
case '--help':
case '-h':
console.log('显示帮助信息...')
process.exit(0)
break
}
}
for (let i = 1; i < args.length; i++) {
const option = args[i]
const next = args[i + 1]
// value exists and isn’t another flag
const hasValue = next !== undefined && !next.startsWith('-')
switch (option) {
case '--output':
case '-o':
if (!hasValue) {
console.error('❌ 错误: --output/-o 需要一个文件路径')
process.exit(1)
}
outputFile = next
i++
break
case '--format':
case '-f':
if (!hasValue) {
console.error('❌ 错误: --format/-f 需要一个值: json | js')
process.exit(1)
}
if (next === 'json' || next === 'js') {
format = next
} else {
console.error(`❌ 错误: 不支持的输出格式 - ${next}`)
process.exit(1)
}
i++
break
case '--help':
case '-h':
console.log('显示帮助信息...')
process.exit(0)
break
default:
if (option.startsWith('-')) {
console.warn(`⚠️ 未知参数: ${option}(已忽略)`)
}
}
}

Comment on lines +8 to +17
"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.js"
},
"files": [
"dist"
],
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Add exports map, engines, and sideEffects for correct dual-publish and better DX

Define explicit ESM/CJS entrypoints via exports, declare Node engine (Vite 5 requires Node 18+), and mark sideEffects to help tree-shaking.

Apply this diff:

   "type": "module",
+  "engines": {
+    "node": ">=18.17"
+  },
   "main": "dist/tiny-engine-vue-to-dsl.cjs",
   "module": "dist/tiny-engine-vue-to-dsl.js",
   "types": "dist/index.d.ts",
+  "exports": {
+    ".": {
+      "types": "./dist/index.d.ts",
+      "import": "./dist/tiny-engine-vue-to-dsl.js",
+      "require": "./dist/tiny-engine-vue-to-dsl.cjs",
+      "default": "./dist/tiny-engine-vue-to-dsl.js"
+    },
+    "./cli": "./dist/cli.js"
+  },
   "bin": {
     "tiny-vue-to-dsl": "dist/cli.js"
   },
+  "sideEffects": false,
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
"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.js"
},
"files": [
"dist"
],
"type": "module",
"engines": {
"node": ">=18.17"
},
"main": "dist/tiny-engine-vue-to-dsl.cjs",
"module": "dist/tiny-engine-vue-to-dsl.js",
"types": "dist/index.d.ts",
"exports": {
".": {
"types": "./dist/index.d.ts",
"import": "./dist/tiny-engine-vue-to-dsl.js",
"require": "./dist/tiny-engine-vue-to-dsl.cjs",
"default": "./dist/tiny-engine-vue-to-dsl.js"
},
"./cli": "./dist/cli.js"
},
"bin": {
"tiny-vue-to-dsl": "dist/cli.js"
},
"sideEffects": false,
"files": [
"dist"
],

Comment on lines +61 to +67
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
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Custom template parser is called without options and may return wrong shape.

Public API implies custom parsers accept (code, options) and template parse returns an array. Pass options and normalize to an array. Otherwise, downstream generateSchema may receive a non-array.

-          templateSchema = this.options.customParsers?.template
-            ? this.options.customParsers.template.parse(sfcResult.template)
+          templateSchema = this.options.customParsers?.template
+            ? this.options.customParsers.template.parse(sfcResult.template, this.options)
             : parseTemplate(sfcResult.template, this.options as any)
+          if (!Array.isArray(templateSchema)) {
+            templateSchema = templateSchema ? [templateSchema] : []
+          }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
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
}
templateSchema = this.options.customParsers?.template
? this.options.customParsers.template.parse(sfcResult.template, this.options)
: parseTemplate(sfcResult.template, this.options as any)
if (!Array.isArray(templateSchema)) {
templateSchema = templateSchema ? [templateSchema] : []
}
🤖 Prompt for AI Agents
In packages/vue-to-dsl/src/converter.ts around lines 61 to 67, the custom
template parser is invoked without passing options and its return value isn't
normalized to an array; update the call to pass this.options (e.g.,
this.options.customParsers.template.parse(sfcResult.template, this.options)) and
after parsing ensure templateSchema is an array (if result is not an array, wrap
it: templateSchema = Array.isArray(result) ? result : [result]); keep the
existing fallback to parseTemplate(sfcResult.template, this.options) and
preserve the existing error push/strictMode throw behavior.

Comment on lines +73 to +79
scriptSchema = this.options.customParsers?.script
? this.options.customParsers.script.parse(scriptContent)
: parseScript(scriptContent, {
isSetup: !!sfcResult.scriptSetup,
...(this.options as any)
})

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Custom script parser is called without options.

Align with default parser call and the declared API by passing options (including isSetup).

-          scriptSchema = this.options.customParsers?.script
-            ? this.options.customParsers.script.parse(scriptContent)
+          scriptSchema = this.options.customParsers?.script
+            ? this.options.customParsers.script.parse(scriptContent, {
+                isSetup: !!sfcResult.scriptSetup,
+                ...(this.options as any)
+              })
             : parseScript(scriptContent, {
                 isSetup: !!sfcResult.scriptSetup,
                 ...(this.options as any)
               })
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
scriptSchema = this.options.customParsers?.script
? this.options.customParsers.script.parse(scriptContent)
: parseScript(scriptContent, {
isSetup: !!sfcResult.scriptSetup,
...(this.options as any)
})
scriptSchema = this.options.customParsers?.script
? this.options.customParsers.script.parse(scriptContent, {
isSetup: !!sfcResult.scriptSetup,
...(this.options as any)
})
: parseScript(scriptContent, {
isSetup: !!sfcResult.scriptSetup,
...(this.options as any)
})
🤖 Prompt for AI Agents
In packages/vue-to-dsl/src/converter.ts around lines 73 to 79, the custom script
parser is being invoked with only the script content and no options; update the
call to pass the same options object used for the default parser (include
isSetup: !!sfcResult.scriptSetup and spread this.options) so the custom parser
receives the same configuration shape as parseScript.

Comment on lines +121 to +125
"componentName": "span",
"props": {
"style": "background-color: [object Event]; color: #8a8e99; font-size: 12px"
},
"children": [
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Data corruption: style contains '[object Event]'.

"background-color: [object Event]" indicates a non-serializable value leaked into style serialization. This is a converter bug and will propagate invalid CSS into the DSL.

Short-term fix in expected schema (to prevent masking the issue, prefer fixing the generator and updating all affected fixtures):

-                    "style": "background-color: [object Event]; color: #8a8e99; font-size: 12px"
+                    "style": "color: #8a8e99; font-size: 12px"

Follow-up in generator (pseudo):

  • When building inline style strings, only accept primitive values (string/number). Skip others.
  • Consider normalizing CSS values and trimming trailing semicolons.

Example guard:

for (const [k, v] of Object.entries(styleObj)) {
  if (typeof v !== 'string' && typeof v !== 'number') continue;
  parts.push(`${toKebab(k)}: ${String(v)}`);
}
🤖 Prompt for AI Agents
In packages/vue-to-dsl/test/testcases/005_createVM/expected/schema.json around
lines 121-125 the inline style value contains "background-color: [object Event]"
which indicates a non-primitive value leaked into style serialization; update
the generator that builds inline style strings to only include primitive values
(strings or numbers) and skip objects, functions, events or other
non-serializable types, normalize property names to kebab-case, trim extraneous
semicolons/spaces, and then regenerate/update the expected fixture(s) so the
schema no longer contains "[object Event]".

Comment on lines +26 to +33
<tiny-form
labelWidth="80px"
labelPosition="top"
:inline="false"
label-position="left "
label-width="150px"
style="border-radius: 0px"
>
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Duplicate and conflicting form props: labelWidth/label-width, labelPosition/label-position

Both camelCase and kebab-case props are set with different values; Vue normalizes them to the same prop so the last one wins unpredictably. Remove duplicates and trailing space in values.

Apply this diff:

-      <tiny-form
-        labelWidth="80px"
-        labelPosition="top"
-        :inline="false"
-        label-position="left "
-        label-width="150px"
-        style="border-radius: 0px"
-      >
+      <tiny-form
+        label-position="left"
+        label-width="150px"
+        :inline="false"
+        style="border-radius: 0px"
+      >

Repeat the same clean-up for other tiny-form usages in this file.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
<tiny-form
labelWidth="80px"
labelPosition="top"
:inline="false"
label-position="left "
label-width="150px"
style="border-radius: 0px"
>
<tiny-form
label-position="left"
label-width="150px"
:inline="false"
style="border-radius: 0px"
>
🤖 Prompt for AI Agents
packages/vue-to-dsl/test/testcases/005_createVM/input/component.vue lines 26-33:
the tiny-form declares duplicate/conflicting props (labelWidth vs label-width
and labelPosition vs label-position) and has a trailing space in "left "; remove
the duplicates so each prop is declared only once (use one naming style
consistently — e.g., keep label-width="150px" and label-position="left"), delete
the other conflicting attributes (e.g., remove labelWidth="80px" and
labelPosition="top"), trim any trailing spaces in prop values, and apply the
same de-duplication and trimming to every other tiny-form usage in this file.

Comment on lines +49 to +51
<span style="background-color: [object Event]; color: #8a8e99; font-size: 12px"
>温馨提示:页面左上角切换区域</span
>
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Invalid CSS in inline style: background-color: [object Event]

This is a runtime artifact string and will render invalid CSS. Replace with a valid color token.

-          <span style="background-color: [object Event]; color: #8a8e99; font-size: 12px"
+          <span style="background-color: transparent; color: #8a8e99; font-size: 12px"
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
<span style="background-color: [object Event]; color: #8a8e99; font-size: 12px"
>温馨提示:页面左上角切换区域</span
>
<span style="background-color: transparent; color: #8a8e99; font-size: 12px"
>温馨提示:页面左上角切换区域</span
>
🤖 Prompt for AI Agents
In packages/vue-to-dsl/test/testcases/005_createVM/input/component.vue around
lines 49 to 51, the inline style contains an invalid value "background-color:
[object Event]" which will produce invalid CSS at runtime; replace that runtime
artifact with a valid color token or value (for example a CSS variable like
var(--color-warning) or a hex/RGB value) or remove the inline background-color
and apply a class instead so the rendered CSS is valid.

Comment on lines +287 to +304
<div v-for="() in state.dataDisk" style="margin-top: 12px; display: flex">
<tiny-icon-panel-mini
style="margin-right: 10px; width: 16px; height: 16px"
fill="currentColor"
></tiny-icon-panel-mini>
<tiny-select
modelValue=""
placeholder="请选择"
style="width: 200px; margin-right: 10px"
:options="[
{ value: '1', label: '黄金糕' },
{ value: '2', label: '双皮奶' }
]"
></tiny-select>
<tiny-input placeholder="请输入" modelValue="" style="width: 120px; margin-right: 10px"></tiny-input>
<span style="color: #575d6c; font-size: 12px; margin-right: 10px">GiB IOPS上限600,IOPS突发上限5,000</span>
<tiny-input placeholder="请输入" modelValue="" style="width: 120px"></tiny-input>
</div>
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Invalid v-for syntax: empty tuple “() in …”

v-for requires an item identifier. Use item or (item, index).

-          <div v-for="() in state.dataDisk" style="margin-top: 12px; display: flex">
+          <div v-for="(_, idx) in state.dataDisk" :key="idx" style="margin-top: 12px; display: flex">

Note: add a key for list rendering stability.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
<div v-for="() in state.dataDisk" style="margin-top: 12px; display: flex">
<tiny-icon-panel-mini
style="margin-right: 10px; width: 16px; height: 16px"
fill="currentColor"
></tiny-icon-panel-mini>
<tiny-select
modelValue=""
placeholder="请选择"
style="width: 200px; margin-right: 10px"
:options="[
{ value: '1', label: '黄金糕' },
{ value: '2', label: '双皮奶' }
]"
></tiny-select>
<tiny-input placeholder="请输入" modelValue="" style="width: 120px; margin-right: 10px"></tiny-input>
<span style="color: #575d6c; font-size: 12px; margin-right: 10px">GiB IOPS上限600,IOPS突发上限5,000</span>
<tiny-input placeholder="请输入" modelValue="" style="width: 120px"></tiny-input>
</div>
<div v-for="(_, idx) in state.dataDisk" :key="idx" style="margin-top: 12px; display: flex">
<tiny-icon-panel-mini
style="margin-right: 10px; width: 16px; height: 16px"
fill="currentColor"
></tiny-icon-panel-mini>
<tiny-select
modelValue=""
placeholder="请选择"
style="width: 200px; margin-right: 10px"
:options="[
{ value: '1', label: '黄金糕' },
{ value: '2', label: '双皮奶' }
]"
></tiny-select>
<tiny-input placeholder="请输入" modelValue="" style="width: 120px; margin-right: 10px"></tiny-input>
<span style="color: #575d6c; font-size: 12px; margin-right: 10px">GiB IOPS上限600,IOPS突发上限5,000</span>
<tiny-input placeholder="请输入" modelValue="" style="width: 120px"></tiny-input>
</div>
🤖 Prompt for AI Agents
In packages/vue-to-dsl/test/testcases/005_createVM/input/component.vue around
lines 287 to 304, the v-for uses an empty tuple "() in state.dataDisk" which is
invalid; replace it with a proper item identifier such as "item in
state.dataDisk" or "(item, index) in state.dataDisk" and add a unique key
binding (e.g. :key="index" or :key="item.id") on the root element inside the
loop for stable list rendering.

Comment on lines +37 to +39
alias: {
'@': '/src'
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Alias path is incorrect; '/src' is not a file-system path.

In Vite, alias targets should be absolute FS paths. Using '/src' will resolve to the OS root, breaking resolution in CI and on Windows.

Apply this diff:

   resolve: {
     alias: {
-      '@': '/src'
+      '@': fileURLToPath(new URL('./src', import.meta.url))
     }
   }

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
packages/vue-to-dsl/vite.config.js lines 37-39: the alias target '/src' is an
absolute OS root path and will break resolution on CI/Windows; change the alias
target to an absolute filesystem path by resolving the project src directory
(e.g. use path.resolve(__dirname, 'src') or import/require path and call
path.resolve with __dirname and 'src'), and add the corresponding require/import
of the path module at the top of the file.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 11

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
packages/build/vite-config/src/vite-plugins/devAliasPlugin.js (1)

9-12: Boolean useSourceAlias will throw when accessing basePath — add type guard

When useSourceAlias is true (per JSDoc it's boolean|Object), accessing useSourceAlias.basePath throws.

Apply:

-  const defaultBasePath = path.resolve(process.cwd(), '..') // parent path
-  const basePath = useSourceAlias.basePath || defaultBasePath
+  const defaultBasePath = path.resolve(process.cwd(), '..') // parent path
+  const basePath =
+    typeof useSourceAlias === 'object' && useSourceAlias && useSourceAlias.basePath
+      ? useSourceAlias.basePath
+      : defaultBasePath
♻️ Duplicate comments (4)
packages/vue-to-dsl/src/converter.ts (4)

97-106: Pass style options (lang/scoped) and options to custom style parser.

-          styleSchema = this.options.customParsers?.style
-            ? this.options.customParsers.style.parse(sfcResult.style)
-            : parseStyle(sfcResult.style, this.options as any)
+          const styleOpts =
+            (sfcResult as any).styleBlocks?.length
+              ? {
+                  lang: (sfcResult as any).styleBlocks[0].lang,
+                  scoped: (sfcResult as any).styleBlocks.some((b: any) => b.scoped)
+                }
+              : {}
+          styleSchema = this.options.customParsers?.style
+            ? this.options.customParsers.style.parse(sfcResult.style, { ...(this.options as any), ...styleOpts })
+            : parseStyle(sfcResult.style, { ...(this.options as any), ...styleOpts })

14-18: Update custom parser typings to accept options.

   customParsers?: {
-    template?: { parse: (code: string) => any }
-    script?: { parse: (code: string) => any }
-    style?: { parse: (code: string) => any }
+    template?: { parse: (code: string, options?: any) => any }
+    script?: { parse: (code: string, options?: any) => any }
+    style?: { parse: (code: string, options?: any) => any }
   }

60-69: Pass options to custom template parser and normalize to array.

-          templateSchema = this.options.customParsers?.template
-            ? this.options.customParsers.template.parse(sfcResult.template)
-            : parseTemplate(sfcResult.template, this.options as any)
+          const tpl = this.options.customParsers?.template
+            ? this.options.customParsers.template.parse(sfcResult.template, this.options as any)
+            : parseTemplate(sfcResult.template, this.options as any)
+          templateSchema = Array.isArray(tpl) ? tpl : tpl ? [tpl] : []

71-80: Pass options (incl. isSetup) to custom script parser.

-          scriptSchema = this.options.customParsers?.script
-            ? this.options.customParsers.script.parse(scriptContent)
+          scriptSchema = this.options.customParsers?.script
+            ? this.options.customParsers.script.parse(scriptContent, {
+                isSetup: !!sfcResult.scriptSetup,
+                ...(this.options as any)
+              })
             : parseScript(scriptContent, {
                 isSetup: !!sfcResult.scriptSetup,
                 ...(this.options as any)
               })
🧹 Nitpick comments (13)
packages/toolbars/generate-code/package.json (1)

30-31: Lazy-load VueToDslConverter in Main.vue
Replace the static import at packages/toolbars/generate-code/src/Main.vue:44 with a dynamic import inside the Upload handler (e.g. const { VueToDslConverter } = await import('@opentiny/tiny-engine-vue-to-dsl')) to keep the initial bundle lean.

packages/toolbars/generate-code/meta.js (1)

7-8: Preserve backward-compat by keeping icon.default.

If any consumer still reads icon.default, this avoids a silent regression while adding download/upload.

     icon: {
-      download: 'flow-download',
-      upload: 'upload'
+      default: 'flow-download',
+      download: 'flow-download',
+      upload: 'upload'
     },
packages/vue-to-dsl/test/full/input/DemoPage.vue (3)

4-4: Use a boolean for Switch’s modelValue to better reflect expected API.

String '' may coerce oddly; a boolean is clearer for fixtures.

-      <tiny-switch modelValue=""></tiny-switch>
+      <tiny-switch :modelValue="false"></tiny-switch>

10-13: Deduplicate Vue imports.

Use named imports only (and drop the unused TinySwitch import) to keep the fixture minimal.

-import { Switch as TinySwitch } from '@opentiny/vue'
-import * as vue from 'vue'
-import { defineProps, defineEmits } from 'vue'
+import { inject, reactive, defineProps, defineEmits } from 'vue'

Then update usages:

-const { t, lowcodeWrap, stores } = vue.inject(I18nInjectionKey).lowcode()
+const { t, lowcodeWrap, stores } = inject(I18nInjectionKey).lowcode()
-const state = vue.reactive({})
+const state = reactive({})

18-21: Optional: guard injection for standalone runs.

If the test harness doesn’t provide I18nInjectionKey, inject(...) returns undefined and this will throw. Add a small guard if you expect isolated execution.

packages/vue-to-dsl/test/full/input/testCanvasRowCol.vue (2)

31-34: Deduplicate Vue imports.

Prefer named imports over * as vue for readability in fixtures.

-import * as vue from 'vue'
-import { defineProps, defineEmits } from 'vue'
+import { inject, reactive, defineProps, defineEmits } from 'vue'

And update below:

-const { t, lowcodeWrap, stores } = vue.inject(I18nInjectionKey).lowcode()
+const { t, lowcodeWrap, stores } = inject(I18nInjectionKey).lowcode()
-const state = vue.reactive({
+const state = reactive({

55-60: Scoped style won’t affect body.

With scoped, selectors are attribute-scoped; body won’t match. Use global style or :global().

-<style scoped>
+<style>
 body {
   background-color: #eef0f5;
   margin-bottom: 80px;
 }
 </style>
packages/vue-to-dsl/test/full/input/createVm.vue (1)

411-415: Unsafe injection access may throw when i18n is not provided.

Guard inject() to avoid runtime errors outside lowcode runtime.

-const { t, lowcodeWrap, stores } = vue.inject(I18nInjectionKey).lowcode()
+const injected = vue.inject(I18nInjectionKey) as any
+const { t, lowcodeWrap, stores } = injected?.lowcode?.() || { t: (s: any) => s, lowcodeWrap: (_: any) => (_: any) => ({}), stores: {} }
packages/vue-to-dsl/test/full/index.test.js (3)

18-20: Test name mismatches behavior.

It converts all .vue files, not “page1.vue and page2.vue”.

-  it('should convert page1.vue and page2.vue and write outputs', async () => {
+  it('should convert all *.vue inputs and write outputs', async () => {

21-23: Debug logs in tests.

Consider removing or gating with env to keep CI output clean.

-    console.log('Input directory files:', inputFiles)
+    // console.debug('Input directory files:', inputFiles)

31-33: Assert result count equals input count.

Quick sanity check to catch silent skips.

     const results = await converter.convertMultipleFiles(inputFilePathList)
+    expect(results.length).toBe(inputFilePathList.length)
packages/toolbars/generate-code/src/Main.vue (1)

277-281: Allow reusing the same useCanvas refs; avoid repeated calls.

Cache once in setup to prevent creating new instances in handlers.

-    const triggerUpload = () => {
+    const triggerUpload = () => {
       fileInputRef.value?.click()
     }

Outside handlers:

-    const { isBlock, getCurrentPage } = useCanvas()
+    const { isBlock, getCurrentPage, pageState, resetBlockCanvasState, resetPageCanvasState } = useCanvas()
packages/vue-to-dsl/src/converter.ts (1)

121-124: Preserve partial results on failure.

Consider returning collected dependencies/warnings even when a conversion error occurs.

-    } catch (error: any) {
-      errors.push(`Conversion error: ${error.message}`)
-      return { schema: null, dependencies: [], errors, warnings }
+    } catch (error: any) {
+      errors.push(`Conversion error: ${error.message}`)
+      return { schema: null, dependencies: [...new Set(dependencies)], errors, warnings }
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 3ce30b1 and f586c32.

📒 Files selected for processing (11)
  • packages/build/vite-config/src/vite-plugins/devAliasPlugin.js (1 hunks)
  • packages/toolbars/generate-code/meta.js (1 hunks)
  • packages/toolbars/generate-code/package.json (1 hunks)
  • packages/toolbars/generate-code/src/Main.vue (5 hunks)
  • packages/vue-to-dsl/package.json (1 hunks)
  • packages/vue-to-dsl/src/constants.ts (1 hunks)
  • packages/vue-to-dsl/src/converter.ts (1 hunks)
  • packages/vue-to-dsl/test/full/index.test.js (1 hunks)
  • packages/vue-to-dsl/test/full/input/DemoPage.vue (1 hunks)
  • packages/vue-to-dsl/test/full/input/createVm.vue (1 hunks)
  • packages/vue-to-dsl/test/full/input/testCanvasRowCol.vue (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • packages/vue-to-dsl/package.json
🧰 Additional context used
🧠 Learnings (4)
📓 Common learnings
Learnt from: chilingling
PR: opentiny/tiny-engine#837
File: packages/vue-generator/src/plugins/genDependenciesPlugin.js:66-66
Timestamp: 2024-09-30T07:51:10.036Z
Learning: In the `tiny-engine` project, `opentiny/tiny-engine-dsl-vue` refers to the current package itself, and importing types from it may cause circular dependencies.
📚 Learning: 2024-09-30T07:51:10.036Z
Learnt from: chilingling
PR: opentiny/tiny-engine#837
File: packages/vue-generator/src/plugins/genDependenciesPlugin.js:66-66
Timestamp: 2024-09-30T07:51:10.036Z
Learning: In the `tiny-engine` project, `opentiny/tiny-engine-dsl-vue` refers to the current package itself, and importing types from it may cause circular dependencies.

Applied to files:

  • packages/toolbars/generate-code/package.json
  • packages/build/vite-config/src/vite-plugins/devAliasPlugin.js
  • packages/toolbars/generate-code/src/Main.vue
📚 Learning: 2025-03-19T03:13:51.520Z
Learnt from: gene9831
PR: opentiny/tiny-engine#1226
File: packages/canvas/container/src/components/CanvasDivider.vue:184-185
Timestamp: 2025-03-19T03:13:51.520Z
Learning: The CSS bug in packages/canvas/container/src/components/CanvasDivider.vue where verLeft already includes "px" but is being appended again in the style object will be fixed in a future update, as confirmed by gene9831.

Applied to files:

  • packages/vue-to-dsl/test/full/input/testCanvasRowCol.vue
📚 Learning: 2025-01-14T06:55:59.692Z
Learnt from: gene9831
PR: opentiny/tiny-engine#1011
File: packages/configurator/src/router-select-configurator/RouterSelectConfigurator.vue:95-98
Timestamp: 2025-01-14T06:55:59.692Z
Learning: The tiny-select component from opentiny/vue library ensures selected options are valid internally, requiring no additional validation in the change handler.

Applied to files:

  • packages/vue-to-dsl/test/full/input/DemoPage.vue
🧬 Code graph analysis (1)
packages/vue-to-dsl/src/converter.ts (6)
packages/vue-to-dsl/src/constants.ts (1)
  • defaultComponentMap (1-14)
packages/vue-to-dsl/src/parser/index.ts (1)
  • parseSFC (4-41)
packages/vue-to-dsl/src/parsers/templateParser.ts (1)
  • parseTemplate (231-238)
packages/vue-to-dsl/src/parsers/scriptParser.ts (1)
  • parseScript (399-427)
packages/vue-to-dsl/src/parsers/styleParser.ts (1)
  • parseStyle (1-6)
packages/vue-to-dsl/src/generator/index.ts (1)
  • generateSchema (140-165)
🔇 Additional comments (3)
packages/build/vite-config/src/vite-plugins/devAliasPlugin.js (1)

63-64: Dev alias '@opentiny/tiny-engine-vue-to-dsl' validated
Entry file exists, package.json name matches alias, no self-imports detected, and workspace includes the package.

packages/vue-to-dsl/test/full/index.test.js (1)

7-9: Guard __dirname for ESM in Vitest tests
Replace the hard-coded __dirname with a runtime fallback:

- const fullDir = __dirname
+ import path from 'path'
+ import { fileURLToPath } from 'url'
+ const fullDir = typeof __dirname !== 'undefined'
+   ? __dirname
+   : path.dirname(fileURLToPath(import.meta.url))

Verify this change works under your Vitest setup (ESM/CJS) before merging.

packages/toolbars/generate-code/src/Main.vue (1)

44-45: Check package name and circular deps.

Import looks good; just ensure this package is external to avoid circular deps noted in prior learnings.

Would you confirm that @opentiny/tiny-engine-vue-to-dsl is published as a separate package and not an alias to the current workspace?

Comment on lines +27 to +33
labelWidth="80px"
labelPosition="top"
:inline="false"
label-position="left "
label-width="150px"
style="border-radius: 0px"
>
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Duplicate/contradictory TinyForm props; trailing space breaks value.

You set both camelCase and kebab-case props, and label-position has a trailing space ("left "). Keep one style and fix the space to avoid incorrect rendering.

-      <tiny-form
-        labelWidth="80px"
-        labelPosition="top"
-        :inline="false"
-        label-position="left "
-        label-width="150px"
-        style="border-radius: 0px"
-      >
+      <tiny-form
+        :inline="false"
+        label-position="left"
+        label-width="150px"
+        style="border-radius: 0px"
+      >
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
labelWidth="80px"
labelPosition="top"
:inline="false"
label-position="left "
label-width="150px"
style="border-radius: 0px"
>
<tiny-form
:inline="false"
label-position="left"
label-width="150px"
style="border-radius: 0px"
>
🤖 Prompt for AI Agents
In packages/vue-to-dsl/test/full/input/createVm.vue around lines 27 to 33, there
are duplicate/conflicting TinyForm props using both camelCase and kebab-case and
the label-position value contains a trailing space; remove the duplicated style
(choose either camelCase or kebab-case and keep only that style for
labelWidth/labelPosition), delete the redundant labelWidth or label-width entry,
and fix the label-position value to "left" (no trailing space) so props are not
contradictory and the value is valid.

Comment on lines +49 to +51
<span style="background-color: [object Event]; color: #8a8e99; font-size: 12px"
>温馨提示:页面左上角切换区域</span
>
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Invalid inline style value renders literally.

background-color: [object Event] is a runtime/stringification bug and will leak to UI.

-          <span style="background-color: [object Event]; color: #8a8e99; font-size: 12px"
+          <span style="background-color: transparent; color: #8a8e99; font-size: 12px"
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
<span style="background-color: [object Event]; color: #8a8e99; font-size: 12px"
>温馨提示:页面左上角切换区域</span
>
<span style="background-color: transparent; color: #8a8e99; font-size: 12px"
>温馨提示:页面左上角切换区域</span
>
🤖 Prompt for AI Agents
In packages/vue-to-dsl/test/full/input/createVm.vue around lines 49 to 51 the
inline style contains background-color: [object Event] which is coming from
passing an Event object into the style value; replace the literal/event-derived
value with a valid CSS color (or remove the inline style), and ensure the
component uses a proper bound style or class instead of stringifying an Event.
Locate where the value is set (likely an event handler passing the event),
change it to use the actual color source (e.g., a data prop like
currentRegionColor or event.target.value) and bind it with :style="{
backgroundColor: currentRegionColor }" or use a CSS class to avoid leaking
[object Event] into the DOM.

Comment on lines +196 to +203
<tiny-form
labelWidth="80px"
labelPosition="top"
:inline="false"
label-position="left "
label-width="150px"
style="border-radius: 0px"
>
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Repeat: TinyForm prop duplication.

-      <tiny-form
-        labelWidth="80px"
-        labelPosition="top"
-        :inline="false"
-        label-position="left "
-        label-width="150px"
-        style="border-radius: 0px"
-      >
+      <tiny-form
+        :inline="false"
+        label-position="left"
+        label-width="150px"
+        style="border-radius: 0px"
+      >
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
<tiny-form
labelWidth="80px"
labelPosition="top"
:inline="false"
label-position="left "
label-width="150px"
style="border-radius: 0px"
>
<tiny-form
:inline="false"
label-position="left"
label-width="150px"
style="border-radius: 0px"
>
🤖 Prompt for AI Agents
In packages/vue-to-dsl/test/full/input/createVm.vue around lines 196 to 203
there are duplicated TinyForm props (labelWidth vs label-width and labelPosition
vs label-position) with conflicting values; remove the duplicate entries and
keep a single, consistent prop form (either camelCase or kebab-case) with the
intended value, and trim any stray whitespace (e.g., change label-position="left
" to label-position="left" or remove the camelCase duplicates and retain
label-width="150px" and label-position="left" if those are desired).

Comment on lines +254 to +261
<tiny-form
labelWidth="80px"
labelPosition="top"
:inline="false"
label-position="left "
label-width="150px"
style="border-radius: 0px"
>
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Repeat: TinyForm prop duplication.

-      <tiny-form
-        labelWidth="80px"
-        labelPosition="top"
-        :inline="false"
-        label-position="left "
-        label-width="150px"
-        style="border-radius: 0px"
-      >
+      <tiny-form
+        :inline="false"
+        label-position="left"
+        label-width="150px"
+        style="border-radius: 0px"
+      >
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
<tiny-form
labelWidth="80px"
labelPosition="top"
:inline="false"
label-position="left "
label-width="150px"
style="border-radius: 0px"
>
<tiny-form
:inline="false"
label-position="left"
label-width="150px"
style="border-radius: 0px"
>
🤖 Prompt for AI Agents
In packages/vue-to-dsl/test/full/input/createVm.vue around lines 254 to 261, the
<tiny-form> element has duplicate/conflicting prop declarations (labelWidth vs
label-width and labelPosition vs label-position) and a trailing space in "left
". Remove the duplicated props so each prop appears only once, choose a single
consistent casing (kebab-case in templates or camelCase if using bind), keep the
intended values (e.g., label-width="150px" and label-position="top" or
labelWidth="150px" and :labelPosition="'top'"), and delete the extra space in
"left " so the final tag has a single, non-conflicting set of props.

Comment on lines +291 to +312
<div v-for="() in state.dataDisk" style="margin-top: 12px; display: flex">
<tiny-icon-panel-mini
style="margin-right: 10px; width: 16px; height: 16px"
fill="currentColor"
></tiny-icon-panel-mini>
<tiny-select
placeholder="请选择"
style="width: 200px; margin-right: 10px"
v-model="state.formData.diskType"
:options="[
{ value: '1', label: '黄金糕' },
{ value: '2', label: '双皮奶' }
]"
></tiny-select>
<tiny-input
placeholder="请输入"
style="width: 120px; margin-right: 10px"
v-model="state.inputValues.dataDiskSize"
></tiny-input>
<span style="color: #575d6c; font-size: 12px; margin-right: 10px">GiB IOPS上限600,IOPS突发上限5,000</span>
<tiny-input placeholder="请输入" style="width: 120px" v-model="state.inputValues.diskLabel"></tiny-input>
</div>
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Invalid v-for; missing alias and key.

v-for="() in state.dataDisk" is invalid and risks parse failures; also add a :key.

-          <div v-for="() in state.dataDisk" style="margin-top: 12px; display: flex">
+          <div v-for="(disk, idx) in state.dataDisk" :key="idx" style="margin-top: 12px; display: flex">
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
<div v-for="() in state.dataDisk" style="margin-top: 12px; display: flex">
<tiny-icon-panel-mini
style="margin-right: 10px; width: 16px; height: 16px"
fill="currentColor"
></tiny-icon-panel-mini>
<tiny-select
placeholder="请选择"
style="width: 200px; margin-right: 10px"
v-model="state.formData.diskType"
:options="[
{ value: '1', label: '黄金糕' },
{ value: '2', label: '双皮奶' }
]"
></tiny-select>
<tiny-input
placeholder="请输入"
style="width: 120px; margin-right: 10px"
v-model="state.inputValues.dataDiskSize"
></tiny-input>
<span style="color: #575d6c; font-size: 12px; margin-right: 10px">GiB IOPS上限600,IOPS突发上限5,000</span>
<tiny-input placeholder="请输入" style="width: 120px" v-model="state.inputValues.diskLabel"></tiny-input>
</div>
<div v-for="(disk, idx) in state.dataDisk" :key="idx" style="margin-top: 12px; display: flex">
<tiny-icon-panel-mini
style="margin-right: 10px; width: 16px; height: 16px"
fill="currentColor"
></tiny-icon-panel-mini>
<tiny-select
placeholder="请选择"
style="width: 200px; margin-right: 10px"
v-model="state.formData.diskType"
:options="[
{ value: '1', label: '黄金糕' },
{ value: '2', label: '双皮奶' }
]"
></tiny-select>
<tiny-input
placeholder="请输入"
style="width: 120px; margin-right: 10px"
v-model="state.inputValues.dataDiskSize"
></tiny-input>
<span style="color: #575d6c; font-size: 12px; margin-right: 10px">GiB IOPS上限600,IOPS突发上限5,000</span>
<tiny-input placeholder="请输入" style="width: 120px" v-model="state.inputValues.diskLabel"></tiny-input>
</div>
🤖 Prompt for AI Agents
In packages/vue-to-dsl/test/full/input/createVm.vue around lines 291 to 312, the
v-for is invalid because it lacks an alias and a key; change v-for="() in
state.dataDisk" to include an item (and optional index) like v-for="(disk, idx)
in state.dataDisk" and add a unique :key (for example :key="disk.id || idx");
adjust any v-model bindings inside the loop to reference the loop variable
(e.g., disk.property or state.dataDisk[idx].property) so each iteration binds
correctly.

Comment on lines +338 to +345
<tiny-form
labelWidth="80px"
labelPosition="top"
:inline="false"
label-position="left "
label-width="150px"
style="border-radius: 0px"
></tiny-form>
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Repeat: TinyForm prop duplication in footer.

-      <tiny-form
-        labelWidth="80px"
-        labelPosition="top"
-        :inline="false"
-        label-position="left "
-        label-width="150px"
-        style="border-radius: 0px"
-      ></tiny-form>
+      <tiny-form
+        :inline="false"
+        label-position="left"
+        label-width="150px"
+        style="border-radius: 0px"
+      ></tiny-form>
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
<tiny-form
labelWidth="80px"
labelPosition="top"
:inline="false"
label-position="left "
label-width="150px"
style="border-radius: 0px"
></tiny-form>
<tiny-form
:inline="false"
label-position="left"
label-width="150px"
style="border-radius: 0px"
></tiny-form>
🤖 Prompt for AI Agents
In packages/vue-to-dsl/test/full/input/createVm.vue around lines 338 to 345, the
<tiny-form> tag contains duplicated props (labelWidth vs label-width and
labelPosition vs label-position) with conflicting values and an extra trailing
space on "left "; remove the duplicate props and keep a single consistent prop
style (use kebab-case in templates: label-width and label-position), ensure only
one value for each (e.g., label-width="150px" and label-position="left" without
trailing space), and delete any redundant inline props so there is no ambiguity.

Comment on lines +49 to +54
const handleElementClick = wrap(function handleElementClick(elementId) {
this.state.uiState.selectedElement = elementId
})

wrap({ handleElementClick })
</script>
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

handleElementClick is defined but never used. Wire it to exercise event conversion.

Hook at least one element to validate the PR’s event-handler conversion path.

-        <canvas-col rowGap="20px" colGap="20px" :grow="true" :shrink="true" widthType="auto"></canvas-col>
+        <canvas-col
+          rowGap="20px"
+          colGap="20px"
+          :grow="true"
+          :shrink="true"
+          widthType="auto"
+          @click="handleElementClick('col-1')"
+        ></canvas-col>
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const handleElementClick = wrap(function handleElementClick(elementId) {
this.state.uiState.selectedElement = elementId
})
wrap({ handleElementClick })
</script>
<canvas-col
rowGap="20px"
colGap="20px"
:grow="true"
:shrink="true"
widthType="auto"
@click="handleElementClick('col-1')"
></canvas-col>
🤖 Prompt for AI Agents
In packages/vue-to-dsl/test/full/input/testCanvasRowCol.vue around lines 49 to
54, handleElementClick is defined and wrapped but never used; attach it to at
least one element in the template so the event-handler conversion path is
exercised. Edit the template to add a click binding (e.g., @click or v-on:click)
on a representative element and call handleElementClick with the element's id or
name (a literal or the element's bound id), keeping the existing wrap({
handleElementClick }) call intact so the test triggers the wrapped handler.
Ensure the element you modify exists in this test file and that the click
binding matches the handler signature.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

♻️ Duplicate comments (3)
packages/toolbars/generate-code/src/Main.vue (3)

471-474: Normalize route for single-file import (ensure leading “/” and no doubles).

-          const safeRoute = `${rawName.replace(/\s+/g, '-').toLowerCase()}`
+          const safeRoute = `/${rawName.replace(/^\/*/, '').replace(/\s+/g, '-').toLowerCase()}`

3-3: Null-guard the icon binding to prevent runtime errors.

options.icon may be undefined; accessing options.icon.download will throw.

-    <toolbar-base content="出码" :icon="options.icon.download || options.icon" :options="options" @click-api="generate">
+    <toolbar-base content="出码" :icon="options?.icon?.download ?? options?.icon" :options="options" @click-api="generate">

466-475: Single-file conversion must check result before use; current code can throw on ...result.schema.

Proceeding without verifying result.schema risks a TypeError. Also include warnings in success.

-          const result = await converter.convertFromString(text, file.name)
+          const result = await converter.convertFromString(text, file.name)
+          if (!result?.schema || (result.errors && result.errors.length)) {
+            useNotify({
+              type: 'error',
+              title: '加载失败',
+              message: (result.errors || []).join('\n') || '转换失败'
+            })
+            return
+          }
+          const warnCount = result.warnings?.length || 0
@@
-              useNotify({ type: 'success', title: '导入成功', message: `已创建新页面:${rawName}` })
+              useNotify({
+                type: 'success',
+                title: '导入成功',
+                message: `已创建新页面:${rawName}${warnCount ? `(${warnCount} 条警告)` : ''}`
+              })

Also applies to: 488-513

🧹 Nitpick comments (3)
packages/toolbars/generate-code/src/Main.vue (3)

13-23: Widen accepted file types or narrow the filename regex for consistency.

You strip .jsx|.tsx in code but the <input> only accepts .vue,.zip. Either allow these types or limit the regex to .vue only.

-        <input ref="fileInputRef" type="file" accept=".vue,.zip" style="display: none" @change="handleFileChange" />
+        <input ref="fileInputRef" type="file" accept=".vue,.zip,.jsx,.tsx" style="display: none" @change="handleFileChange" />

90-104: Prefer switching by page ID when available.

Name-based lookup can be ambiguous; accept an optional id and fall back to name.

If useful, I can provide a small helper to resolve and switch by id first.


280-283: Drop console.error in UI code; rely on useNotify (aligns with toolbar practices).

Keeps logs clean and follows prior guidance from preview toolbar.

-        // eslint-disable-next-line no-console
-        console.error(error)
         useNotify({ type: 'error', title: '代码生成失败', message: error?.message || error })
@@
-        // eslint-disable-next-line no-console
-        console.error(error)
         useNotify({ type: 'error', title: '代码保存失败', message: error?.message || error })
@@
-        // eslint-disable-next-line no-console
-        console.error(error)
         useNotify({ type: 'error', title: '加载失败', message: error?.message || String(error) })

Also applies to: 297-300, 535-537

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between b188159 and d3cc711.

📒 Files selected for processing (1)
  • packages/toolbars/generate-code/src/Main.vue (5 hunks)
🧰 Additional context used
🧠 Learnings (5)
📓 Common learnings
Learnt from: chilingling
PR: opentiny/tiny-engine#837
File: packages/vue-generator/src/plugins/genDependenciesPlugin.js:66-66
Timestamp: 2024-09-30T07:51:10.036Z
Learning: In the `tiny-engine` project, `opentiny/tiny-engine-dsl-vue` refers to the current package itself, and importing types from it may cause circular dependencies.
📚 Learning: 2024-10-10T02:48:10.881Z
Learnt from: yy-wow
PR: opentiny/tiny-engine#850
File: packages/toolbars/preview/src/Main.vue:0-0
Timestamp: 2024-10-10T02:48:10.881Z
Learning: 在 `packages/toolbars/preview/src/Main.vue` 文件中,使用 `useNotify` 而不是 `console` 来记录错误日志。

Applied to files:

  • packages/toolbars/generate-code/src/Main.vue
📚 Learning: 2025-01-14T06:59:23.602Z
Learnt from: rhlin
PR: opentiny/tiny-engine#1011
File: packages/canvas/render/src/page-block-function/methods.ts:9-21
Timestamp: 2025-01-14T06:59:23.602Z
Learning: The code in packages/canvas/render/src/page-block-function/methods.ts is migrated code that should not be modified during the migration phase. Error handling improvements can be addressed in future PRs.

Applied to files:

  • packages/toolbars/generate-code/src/Main.vue
📚 Learning: 2024-10-10T02:47:46.239Z
Learnt from: yy-wow
PR: opentiny/tiny-engine#850
File: packages/toolbars/preview/src/Main.vue:16-16
Timestamp: 2024-10-10T02:47:46.239Z
Learning: In `packages/toolbars/preview/src/Main.vue`, within the `preview` function, the `getMergeMeta` method is used at lines 64 and 65 to retrieve `engine.config` configurations.

Applied to files:

  • packages/toolbars/generate-code/src/Main.vue
📚 Learning: 2024-09-30T07:51:10.036Z
Learnt from: chilingling
PR: opentiny/tiny-engine#837
File: packages/vue-generator/src/plugins/genDependenciesPlugin.js:66-66
Timestamp: 2024-09-30T07:51:10.036Z
Learning: In the `tiny-engine` project, `opentiny/tiny-engine-dsl-vue` refers to the current package itself, and importing types from it may cause circular dependencies.

Applied to files:

  • packages/toolbars/generate-code/src/Main.vue
🔇 Additional comments (4)
packages/toolbars/generate-code/src/Main.vue (4)

25-31: Overwrite dialog wiring looks good.


87-89: Good: hidden file input ref for programmatic trigger.


545-611: Overwrite/creation flow looks solid; route normalization applied and tree refresh handled.


637-641: Styling LGTM.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

🧹 Nitpick comments (2)
packages/toolbars/generate-code/src/Main.vue (2)

18-20: Avoid arrow funcs in template handlers.

Bind directly to reduce per-render allocations.

-        <div class="option-item" @click="() => triggerUpload('file')">Vue 文件</div>
-        <div class="option-item" @click="() => triggerUpload('directory')">项目目录</div>
-        <div class="option-item" @click="() => triggerUpload('zip')">项目压缩包</div>
+        <div class="option-item" @click="triggerUpload('file')">Vue 文件</div>
+        <div class="option-item" @click="triggerUpload('directory')">项目目录</div>
+        <div class="option-item" @click="triggerUpload('zip')">项目压缩包</div>

697-697: Scoped style won’t affect popper content; use :deep selector.

Popover DOM is teleported; scoped styles won’t apply.

-.option-popper {
+:deep(.option-popper) {
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between d3cc711 and 70dce15.

⛔ Files ignored due to path filters (1)
  • mockServer/src/database/pages.db is excluded by !**/*.db
📒 Files selected for processing (2)
  • packages/toolbars/generate-code/src/Main.vue (5 hunks)
  • packages/vue-to-dsl/src/converter.ts (1 hunks)
🧰 Additional context used
🧠 Learnings (10)
📓 Common learnings
Learnt from: chilingling
PR: opentiny/tiny-engine#837
File: packages/vue-generator/src/plugins/genDependenciesPlugin.js:66-66
Timestamp: 2024-09-30T07:51:10.036Z
Learning: In the `tiny-engine` project, `opentiny/tiny-engine-dsl-vue` refers to the current package itself, and importing types from it may cause circular dependencies.
📚 Learning: 2024-10-10T02:48:10.881Z
Learnt from: yy-wow
PR: opentiny/tiny-engine#850
File: packages/toolbars/preview/src/Main.vue:0-0
Timestamp: 2024-10-10T02:48:10.881Z
Learning: 在 `packages/toolbars/preview/src/Main.vue` 文件中,使用 `useNotify` 而不是 `console` 来记录错误日志。

Applied to files:

  • packages/toolbars/generate-code/src/Main.vue
📚 Learning: 2025-01-14T06:59:23.602Z
Learnt from: rhlin
PR: opentiny/tiny-engine#1011
File: packages/canvas/render/src/page-block-function/methods.ts:9-21
Timestamp: 2025-01-14T06:59:23.602Z
Learning: The code in packages/canvas/render/src/page-block-function/methods.ts is migrated code that should not be modified during the migration phase. Error handling improvements can be addressed in future PRs.

Applied to files:

  • packages/toolbars/generate-code/src/Main.vue
📚 Learning: 2024-10-10T02:47:46.239Z
Learnt from: yy-wow
PR: opentiny/tiny-engine#850
File: packages/toolbars/preview/src/Main.vue:16-16
Timestamp: 2024-10-10T02:47:46.239Z
Learning: In `packages/toolbars/preview/src/Main.vue`, within the `preview` function, the `getMergeMeta` method is used at lines 64 and 65 to retrieve `engine.config` configurations.

Applied to files:

  • packages/toolbars/generate-code/src/Main.vue
📚 Learning: 2024-09-30T07:51:10.036Z
Learnt from: chilingling
PR: opentiny/tiny-engine#837
File: packages/vue-generator/src/plugins/genDependenciesPlugin.js:66-66
Timestamp: 2024-09-30T07:51:10.036Z
Learning: In the `tiny-engine` project, `opentiny/tiny-engine-dsl-vue` refers to the current package itself, and importing types from it may cause circular dependencies.

Applied to files:

  • packages/toolbars/generate-code/src/Main.vue
📚 Learning: 2025-01-14T06:49:00.797Z
Learnt from: gene9831
PR: opentiny/tiny-engine#1011
File: packages/configurator/src/router-select-configurator/RouterSelectConfigurator.vue:63-73
Timestamp: 2025-01-14T06:49:00.797Z
Learning: In the tiny-engine project, the SvgIcon component is globally registered and available throughout Vue components without requiring explicit imports.

Applied to files:

  • packages/toolbars/generate-code/src/Main.vue
📚 Learning: 2025-01-14T06:50:21.158Z
Learnt from: rhlin
PR: opentiny/tiny-engine#1011
File: packages/canvas/render/src/data-function/parser.ts:191-195
Timestamp: 2025-01-14T06:50:21.158Z
Learning: The `newFn` function in `packages/canvas/render/src/data-function/parser.ts` has a known security issue with evaluating untrusted code through `data.value`. This was identified during code review but intentionally deferred as the original implementation was kept during code refactoring.

Applied to files:

  • packages/vue-to-dsl/src/converter.ts
📚 Learning: 2025-01-14T06:58:38.661Z
Learnt from: rhlin
PR: opentiny/tiny-engine#1011
File: packages/canvas/render/src/data-utils.ts:8-12
Timestamp: 2025-01-14T06:58:38.661Z
Learning: The use of Function constructor in `packages/canvas/render/src/data-utils.ts` is part of migrated code and was intentionally kept as-is during migration, despite potential security concerns.

Applied to files:

  • packages/vue-to-dsl/src/converter.ts
📚 Learning: 2025-01-14T08:45:57.032Z
Learnt from: rhlin
PR: opentiny/tiny-engine#1011
File: packages/canvas/render/src/application-function/global-state.ts:12-25
Timestamp: 2025-01-14T08:45:57.032Z
Learning: The code in `packages/canvas/render/src/application-function/global-state.ts` is migrated from an existing codebase and should be handled with care when making modifications.

Applied to files:

  • packages/vue-to-dsl/src/converter.ts
📚 Learning: 2025-01-14T06:55:14.457Z
Learnt from: rhlin
PR: opentiny/tiny-engine#1011
File: packages/canvas/render/src/canvas-function/design-mode.ts:6-13
Timestamp: 2025-01-14T06:55:14.457Z
Learning: The code in `packages/canvas/render/src/canvas-function/design-mode.ts` is migrated code that should be preserved in its current form during the migration process. Refactoring suggestions for type safety and state management improvements should be considered in future PRs.

Applied to files:

  • packages/vue-to-dsl/src/converter.ts
🧬 Code graph analysis (1)
packages/vue-to-dsl/src/converter.ts (6)
packages/vue-to-dsl/src/constants.ts (1)
  • defaultComponentMap (1-16)
packages/vue-to-dsl/src/parser/index.ts (1)
  • parseSFC (4-41)
packages/vue-to-dsl/src/parsers/templateParser.ts (1)
  • parseTemplate (231-238)
packages/vue-to-dsl/src/parsers/scriptParser.ts (1)
  • parseScript (399-427)
packages/vue-to-dsl/src/parsers/styleParser.ts (1)
  • parseStyle (1-6)
packages/vue-to-dsl/src/generator/index.ts (2)
  • generateSchema (141-166)
  • generateAppSchema (168-183)
🔇 Additional comments (13)
packages/toolbars/generate-code/src/Main.vue (6)

390-396: Guard app conversion result and surface failures early.

Don’t proceed when conversion fails or returns an invalid shape.

     const processAppSchema = async (appSchema: any) => {
       // 将 appSchema 应用到全局
       const { appSchemaState } = useResource()
+      const warnCount = Array.isArray(appSchema?.warnings) ? appSchema.warnings.length : 0
+      if (!appSchema || typeof appSchema !== 'object' || (!appSchema.pageSchema && !appSchema.i18n)) {
+        useNotify({ type: 'error', title: '加载失败', message: '转换失败' })
+        return
+      }

516-523: Include warning count in success message.

Show warning count when present.

-          useNotify({ type: 'success', title: '导入成功', message: `已更新全局配置(未检测到页面)` })
+          useNotify({
+            type: 'success',
+            title: '导入成功',
+            message: `已更新全局配置(未检测到页面)${warnCount ? `(${warnCount} 条警告)` : ''}`
+          })
         } else {
           await switchToPageByName(chosen?.meta?.name || chosen?.fileName)
           useNotify({
             type: 'success',
             title: '导入成功',
-            message: `已创建页面并加载:${chosen?.meta?.name || '页面'}`
+            message: `已创建页面并加载:${chosen?.meta?.name || '页面'}${warnCount ? `(${warnCount} 条警告)` : ''}`
           })
         }

529-536: Single-file: guard conversion errors and warn on success.

Abort on errors; include warning count on success.

     const processSingleFile = async (file: File, converter: VueToDslConverter) => {
       const text = await file.text()
-      const result = await converter.convertFromString(text, file.name)
+      const result = await converter.convertFromString(text, file.name)
+      if (!result?.schema || (result.errors && result.errors.length)) {
+        useNotify({
+          type: 'error',
+          title: '加载失败',
+          message: (result.errors || []).join('\n') || '转换失败'
+        })
+        return
+      }
+      const warnCount = result.warnings?.length || 0

534-534: Normalize route to start with “/” (aligns with ZIP/dir flow).

-      const safeRoute = `${rawName.replace(/\s+/g, '-').toLowerCase()}`
+      const safeRoute = `/${rawName.replace(/^\/+/, '').replace(/\s+/g, '-').toLowerCase()}`

574-575: Include warnings in success notices for single-file flow.

-          useNotify({ type: 'success', title: '导入成功', message: `已创建新页面:${rawName}` })
+          useNotify({
+            type: 'success',
+            title: '导入成功',
+            message: `已创建新页面:${rawName}${warnCount ? `(${warnCount} 条警告)` : ''}`
+          })
@@
-        useNotify({ type: 'success', title: '导入成功', message: `已创建新页面:${rawName}` })
+        useNotify({
+          type: 'success',
+          title: '导入成功',
+          message: `已创建新页面:${rawName}${warnCount ? `(${warnCount} 条警告)` : ''}`
+        })

Also applies to: 593-594


3-3: Fix potential runtime error: safely access icon.

options.icon can be undefined (default props = {}), causing a crash. Align with Line 15.

-    <toolbar-base content="出码" :icon="options.icon.download || options.icon" :options="options" @click-api="generate">
+    <toolbar-base content="出码" :icon="options?.icon?.download ?? options?.icon" :options="options" @click-api="generate">
packages/vue-to-dsl/src/converter.ts (7)

16-20: Broaden custom parser typings to accept options; template may return array.

   customParsers?: {
-    template?: { parse: (code: string) => any }
-    script?: { parse: (code: string) => any }
-    style?: { parse: (code: string) => any }
+    template?: { parse: (code: string, options?: any) => any | any[] }
+    script?: { parse: (code: string, options?: any) => any }
+    style?: { parse: (code: string, options?: any) => any }
   }

62-71: Pass options to custom template parser and normalize to array.

       if (sfcResult.template) {
         try {
           templateSchema = this.options.customParsers?.template
-            ? this.options.customParsers.template.parse(sfcResult.template)
+            ? this.options.customParsers.template.parse(sfcResult.template, this.options as any)
             : parseTemplate(sfcResult.template, this.options as any)
+          if (!Array.isArray(templateSchema)) {
+            templateSchema = templateSchema ? [templateSchema] : []
+          }
         } catch (error: any) {

76-82: Pass options to custom script parser (align with default).

           scriptSchema = this.options.customParsers?.script
-            ? this.options.customParsers.script.parse(scriptContent)
+            ? this.options.customParsers.script.parse(scriptContent, {
+                isSetup: !!sfcResult.scriptSetup,
+                ...(this.options as any)
+              })
             : parseScript(scriptContent, {

101-103: Pass options to custom style parser.

           styleSchema = this.options.customParsers?.style
-            ? this.options.customParsers.style.parse(sfcResult.style)
+            ? this.options.customParsers.style.parse(sfcResult.style, this.options as any)
             : parseStyle(sfcResult.style, this.options as any)

110-116: Don’t mutate instance options with fileName; derive a local options object.

-      // 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)
+      const genOptions = {
+        ...(this.options as any),
+        fileName: fileName ? fileName.replace(/\.vue$/i, '') : (this.options as any).fileName
+      }
+      const schema = await generateSchema(templateSchema, scriptSchema, styleSchema, genOptions)

283-286: Critical: avoid executing untrusted code via Function(...) for store state. Use a safe object-literal parser.

-            const objText = stateMatch[1]
-            const stateObj = Function(`return (${objText})`)()
+            const objText = stateMatch[1]
+            const stateObj = safeParseObjectLiteral(objText)
@@
-              const objText = stateMatch[1]
-              const stateObj = Function(`return (${objText})`)()
+              const objText = stateMatch[1]
+              const stateObj = safeParseObjectLiteral(objText)
@@
-            const objText = stateMatch[1]
-            const stateObj = Function(`return (${objText})`)()
+            const objText = stateMatch[1]
+            const stateObj = safeParseObjectLiteral(objText)

Additions required elsewhere in this module:

// at top-level imports
import { parse as babelParse } from '@babel/parser'

// helper (outside the class)
function safeParseObjectLiteral(objText: string): any {
  try {
    const ast = babelParse(`(${objText})`, { sourceType: 'module', plugins: ['typescript'] as any })
    // @ts-ignore
    const expr: any = (ast as any).program.body[0].expression
    if (!expr || expr.type !== 'ObjectExpression') return {}
    const toValue = (node: any): any => {
      switch (node.type) {
        case 'StringLiteral':
        case 'NumericLiteral':
        case 'BooleanLiteral':
        case 'NullLiteral':
          return node.value
        case 'ObjectExpression':
          return Object.fromEntries(
            node.properties.map((p: any) => [p.key.name || p.key.value, toValue(p.value)])
          )
        case 'ArrayExpression':
          return node.elements.map((el: any) => toValue(el))
        default:
          return undefined
      }
    }
    return toValue(expr)
  } catch {
    return {}
  }
}

Also applies to: 487-489, 713-714


548-588: Clean up temp directory after ZIP conversion (leak).

-    // 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)
+    // 1) Unzip into a temp directory
+    const tmpBase = await fs.mkdtemp(path.join(os.tmpdir(), 'vue-to-dsl-'))
+    try {
+      const zip = await JSZip.loadAsync(zipBuffer as any)
@@
-    // 3) Delegate to convertAppDirectory
-    const schema = await this.convertAppDirectory(appRoot)
-    return schema
+      // 3) Delegate to convertAppDirectory
+      const schema = await this.convertAppDirectory(appRoot)
+      return schema
+    } finally {
+      await fs.rm(tmpBase, { recursive: true, force: true }).catch(() => {})
+    }

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Nitpick comments (1)
packages/vue-to-dsl/src/converter.ts (1)

612-626: Dynamic regex from .gitignore patterns can be ReDoS-prone; prefer a battle-tested lib.

Use ignore/picomatch/minimatch to parse .gitignore semantics safely.

Example:

// npm i ignore (or ensure it’s available in browser build)
import ignore from 'ignore'
const ig = ignore().add(gitignoreContent)
return (p: string) => ig.ignores(p) === false
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 70dce15 and 43ebd58.

📒 Files selected for processing (1)
  • packages/vue-to-dsl/src/converter.ts (1 hunks)
🧰 Additional context used
🧠 Learnings (5)
📓 Common learnings
Learnt from: chilingling
PR: opentiny/tiny-engine#837
File: packages/vue-generator/src/plugins/genDependenciesPlugin.js:66-66
Timestamp: 2024-09-30T07:51:10.036Z
Learning: In the `tiny-engine` project, `opentiny/tiny-engine-dsl-vue` refers to the current package itself, and importing types from it may cause circular dependencies.
📚 Learning: 2025-01-14T06:50:21.158Z
Learnt from: rhlin
PR: opentiny/tiny-engine#1011
File: packages/canvas/render/src/data-function/parser.ts:191-195
Timestamp: 2025-01-14T06:50:21.158Z
Learning: The `newFn` function in `packages/canvas/render/src/data-function/parser.ts` has a known security issue with evaluating untrusted code through `data.value`. This was identified during code review but intentionally deferred as the original implementation was kept during code refactoring.

Applied to files:

  • packages/vue-to-dsl/src/converter.ts
📚 Learning: 2025-01-14T06:58:38.661Z
Learnt from: rhlin
PR: opentiny/tiny-engine#1011
File: packages/canvas/render/src/data-utils.ts:8-12
Timestamp: 2025-01-14T06:58:38.661Z
Learning: The use of Function constructor in `packages/canvas/render/src/data-utils.ts` is part of migrated code and was intentionally kept as-is during migration, despite potential security concerns.

Applied to files:

  • packages/vue-to-dsl/src/converter.ts
📚 Learning: 2025-01-14T08:45:57.032Z
Learnt from: rhlin
PR: opentiny/tiny-engine#1011
File: packages/canvas/render/src/application-function/global-state.ts:12-25
Timestamp: 2025-01-14T08:45:57.032Z
Learning: The code in `packages/canvas/render/src/application-function/global-state.ts` is migrated from an existing codebase and should be handled with care when making modifications.

Applied to files:

  • packages/vue-to-dsl/src/converter.ts
📚 Learning: 2025-01-14T06:55:14.457Z
Learnt from: rhlin
PR: opentiny/tiny-engine#1011
File: packages/canvas/render/src/canvas-function/design-mode.ts:6-13
Timestamp: 2025-01-14T06:55:14.457Z
Learning: The code in `packages/canvas/render/src/canvas-function/design-mode.ts` is migrated code that should be preserved in its current form during the migration process. Refactoring suggestions for type safety and state management improvements should be considered in future PRs.

Applied to files:

  • packages/vue-to-dsl/src/converter.ts
🧬 Code graph analysis (1)
packages/vue-to-dsl/src/converter.ts (6)
packages/vue-to-dsl/src/constants.ts (1)
  • defaultComponentMap (1-16)
packages/vue-to-dsl/src/parser/index.ts (1)
  • parseSFC (4-41)
packages/vue-to-dsl/src/parsers/templateParser.ts (1)
  • parseTemplate (231-238)
packages/vue-to-dsl/src/parsers/scriptParser.ts (1)
  • parseScript (399-427)
packages/vue-to-dsl/src/parsers/styleParser.ts (1)
  • parseStyle (1-6)
packages/vue-to-dsl/src/generator/index.ts (2)
  • generateSchema (141-166)
  • generateAppSchema (168-183)
🪛 ast-grep (0.39.5)
packages/vue-to-dsl/src/converter.ts

[warning] 621-621: Regular expression constructed from variable input detected. This can lead to Regular Expression Denial of Service (ReDoS) attacks if the variable contains malicious patterns. Use libraries like 'recheck' to validate regex safety or use static patterns.
Context: new RegExp(^${regexString})
Note: [CWE-1333] Inefficient Regular Expression Complexity [REFERENCES]
- https://owasp.org/www-community/attacks/Regular_expression_Denial_of_Service_-_ReDoS
- https://cwe.mitre.org/data/definitions/1333.html

(regexp-from-variable)


[warning] 624-624: Regular expression constructed from variable input detected. This can lead to Regular Expression Denial of Service (ReDoS) attacks if the variable contains malicious patterns. Use libraries like 'recheck' to validate regex safety or use static patterns.
Context: new RegExp(^${regexString}(/.*)?$)
Note: [CWE-1333] Inefficient Regular Expression Complexity [REFERENCES]
- https://owasp.org/www-community/attacks/Regular_expression_Denial_of_Service_-_ReDoS
- https://cwe.mitre.org/data/definitions/1333.html

(regexp-from-variable)

🔇 Additional comments (9)
packages/vue-to-dsl/src/converter.ts (9)

12-25: Broaden custom parser typings to accept options and async; template may return array.

Align types with actual usage and expected extensibility.

 export interface VueToSchemaOptions {
   componentMap?: Record<string, string>
   preserveComments?: boolean
   strictMode?: boolean
   customParsers?: {
-    template?: { parse: (code: string) => any }
-    script?: { parse: (code: string) => any }
-    style?: { parse: (code: string) => any }
+    template?: { parse: (code: string, options?: any) => any | any[] | Promise<any | any[]> }
+    script?: { parse: (code: string, options?: any) => any | Promise<any> }
+    style?: { parse: (code: string, options?: any) => any | Promise<any> }
   }
   fileName?: string
   path?: string
   title?: string
   description?: string
 }

62-71: Pass options to custom template parser and normalize to array; support async.

Prevents downstream shape mismatches.

       if (sfcResult.template) {
         try {
-          templateSchema = this.options.customParsers?.template
-            ? this.options.customParsers.template.parse(sfcResult.template)
-            : parseTemplate(sfcResult.template, this.options as any)
+          if (this.options.customParsers?.template) {
+            const res = await this.options.customParsers.template.parse(
+              sfcResult.template,
+              this.options as any
+            )
+            templateSchema = Array.isArray(res) ? res : res ? [res] : []
+          } else {
+            templateSchema = parseTemplate(sfcResult.template, this.options as any)
+          }
         } catch (error: any) {

76-82: Pass options to custom script parser; support async.

Keeps parity with default parser.

-          scriptSchema = this.options.customParsers?.script
-            ? this.options.customParsers.script.parse(scriptContent)
-            : parseScript(scriptContent, {
-                isSetup: !!sfcResult.scriptSetup,
-                ...(this.options as any)
-              })
+          scriptSchema = this.options.customParsers?.script
+            ? await this.options.customParsers.script.parse(scriptContent, {
+                isSetup: !!sfcResult.scriptSetup,
+                ...(this.options as any)
+              })
+            : parseScript(scriptContent, {
+                isSetup: !!sfcResult.scriptSetup,
+                ...(this.options as any)
+              })

99-108: Pass options to custom style parser; preserve scoped/lang from SFC blocks; support async.

Otherwise scoped/lang are lost.

       if (sfcResult.style) {
         try {
-          styleSchema = this.options.customParsers?.style
-            ? this.options.customParsers.style.parse(sfcResult.style)
-            : parseStyle(sfcResult.style, this.options as any)
+          const styleOpts = {
+            ...(this.options as any),
+            scoped: Array.isArray(sfcResult.styleBlocks)
+              ? sfcResult.styleBlocks.some((b: any) => !!b.scoped)
+              : false,
+            lang:
+              (Array.isArray(sfcResult.styleBlocks) && (sfcResult.styleBlocks[0] as any)?.lang) ||
+              'css'
+          }
+          styleSchema = this.options.customParsers?.style
+            ? await this.options.customParsers.style.parse(sfcResult.style, styleOpts)
+            : parseStyle(sfcResult.style, styleOpts)
         } catch (error: any) {

110-116: Don’t mutate instance options; derive a generator options object.

Avoids cross-call side effects.

-      // 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)
+      const genOptions = {
+        ...(this.options as any),
+        fileName: fileName ? fileName.replace(/\.vue$/i, '') : (this.options as any).fileName
+      }
+      const schema = await generateSchema(templateSchema, scriptSchema, styleSchema, genOptions)

585-588: Ensure temp dir cleanup after unzip.

Avoid leaked temp directories on errors.

-    // 3) Delegate to convertAppDirectory
-    const schema = await this.convertAppDirectory(appRoot)
-    return schema
+    // 3) Delegate to convertAppDirectory with cleanup
+    let schema: any
+    try {
+      schema = await this.convertAppDirectory(appRoot)
+    } finally {
+      await fs.rm(tmpBase, { recursive: true, force: true }).catch(() => {})
+    }
+    return schema

281-286: RCE: avoid evaluating untrusted code with Function when parsing store state.

Replace with a safe AST-based object literal parser.

-            const objText = stateMatch[1]
-            const stateObj = Function(`return (${objText})`)()
-            entry.state = stateObj
+            const objText = stateMatch[1]
+            const stateObj = safeParseObjectLiteral(objText)
+            entry.state = stateObj

Add this helper and import (top-level, outside the class):

import { parse as babelParse } from '@babel/parser'

function safeParseObjectLiteral(objText: string): any {
  try {
    const ast: any = babelParse(`(${objText})`, { sourceType: 'module', plugins: ['typescript'] as any })
    const expr = ast.program.body[0].expression
    if (!expr || expr.type !== 'ObjectExpression') return {}
    const toValue = (node: any): any => {
      switch (node?.type) {
        case 'StringLiteral':
        case 'NumericLiteral':
        case 'BooleanLiteral':
          return node.value
        case 'NullLiteral':
          return null
        case 'ObjectExpression':
          return Object.fromEntries(
            node.properties
              .filter((p: any) => p.type === 'ObjectProperty')
              .map((p: any) => [
                p.key.type === 'Identifier' ? p.key.name : p.key.value,
                toValue(p.value)
              ])
          )
        case 'ArrayExpression':
          return node.elements.map((el: any) => toValue(el))
        default:
          return undefined
      }
    }
    const v = toValue(expr)
    return v && typeof v === 'object' ? v : {}
  } catch {
    return {}
  }
}

487-489: Apply the same safe parsing when unzipping app (browser path).

Mirror the fix here.

-              const objText = stateMatch[1]
-              const stateObj = Function(`return (${objText})`)()
-              entry.state = stateObj
+              const objText = stateMatch[1]
+              const stateObj = safeParseObjectLiteral(objText)
+              entry.state = stateObj

5-6: No mismatch — two distinct exports are intended. generator imports defaultComponentsMap (array of package/component descriptors); converter imports defaultComponentMap (tag → component name map). No change required.

Comment on lines +553 to +571
const fileEntries: string[] = []
const writeTasks: Promise<any>[] = []
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)
})()
)
}
})
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Zip Slip: sanitize zip paths before writing to disk.

Prevent directory traversal during unzip.

-      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)
-          })()
-        )
-      }
+      const destPath = path.resolve(tmpBase, relPath)
+      if (!destPath.startsWith(tmpBase + path.sep) && destPath !== tmpBase) {
+        return // block path traversal
+      }
+      if (file.dir) {
+        writeTasks.push(fs.mkdir(destPath, { recursive: true }))
+      } else {
+        fileEntries.push(relPath)
+        writeTasks.push(
+          (async () => {
+            await fs.mkdir(path.dirname(destPath), { recursive: true })
+            const content = await file.async('nodebuffer')
+            await fs.writeFile(destPath, content)
+          })()
+        )
+      }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const fileEntries: string[] = []
const writeTasks: Promise<any>[] = []
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)
})()
)
}
})
const fileEntries: string[] = []
const writeTasks: Promise<any>[] = []
zip.forEach((relPath, file) => {
// Skip macOS metadata
if (relPath.startsWith('__MACOSX/')) return
const destPath = path.resolve(tmpBase, relPath)
if (!destPath.startsWith(tmpBase + path.sep) && destPath !== tmpBase) {
return // block path traversal
}
if (file.dir) {
writeTasks.push(fs.mkdir(destPath, { recursive: true }))
} else {
fileEntries.push(relPath)
writeTasks.push(
(async () => {
await fs.mkdir(path.dirname(destPath), { recursive: true })
const content = await file.async('nodebuffer')
await fs.writeFile(destPath, content)
})()
)
}
})

Comment on lines +763 to +764
const stateObj = Function(`return (${objText})`)()
entry.state = stateObj
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Apply the same safe parsing in directory import (browser).

-            const objText = stateMatch[1]
-            const stateObj = Function(`return (${objText})`)()
+            const objText = stateMatch[1]
+            const stateObj = safeParseObjectLiteral(objText)

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In packages/vue-to-dsl/src/converter.ts around lines 763-764, replace the unsafe
usage of Function(`return (${objText})`)() with the project's existing safe
parsing helper used elsewhere for browser directory imports: import/Call that
safe parser (e.g., safeParseExpression or parseExpression) and wrap it in a
try/catch to assign entry.state only on successful parse, returning a sensible
fallback (null or {}) and logging or preserving the original text on parse
failure.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

♻️ Duplicate comments (11)
packages/vue-to-dsl/test/testcases/001_simple/input/component.vue (1)

1-35: Past review comment is outdated and no longer applicable.

The past review flagged issues with a count variable and reactive/ref mismatches, but the current code uses switchStatus with correct reactive usage. The file appears to have been completely rewritten since that review.

packages/vue-to-dsl/test/testcases/002_createVM/input/component.vue (3)

26-33: Duplicate and conflicting form props: labelWidth/label-width, labelPosition/label-position

Multiple TinyForm instances declare both camelCase and kebab-case versions of the same props with different values. Vue normalizes these to the same prop, so the last declaration wins unpredictably. Additionally, "left " contains a trailing space.

Also applies to: 82-89, 196-203, 254-261, 278-285, 330-337


49-51: Invalid CSS in inline style: background-color: [object Event]

This runtime artifact will render invalid CSS.


287-304: Invalid v-for syntax: empty tuple "() in …"

The v-for uses an empty tuple which is invalid syntax, and lacks a :key binding.

packages/vue-to-dsl/test/testcases/002_createVM/expected/schema.json (2)

60-67: Duplicate/colliding props: both camelCase and kebab-case emitted.

Props include both labelWidth + label-width and labelPosition + label-position. The converter should normalize to a single canonical casing and deduplicate.

Also applies to: 198-205, 537-544, 666-673, 745-752, 906-913


118-122: Data corruption: style contains '[object Event]'.

The converter leaked a non-serializable value into style serialization, producing invalid CSS in the DSL schema.

packages/vue-to-dsl/src/generator/index.ts (1)

118-131: Sanitizer is a no‑op but comment claims newline removal

Either implement selective sanitization (skip css and any { type: 'JSFunction' } bodies) or update the comment to reflect current behavior. Past review explicitly asked to preserve css and function strings.

-// Deeply sanitize all string values in the schema
+// Deeply sanitize string values (skip css and JSFunction bodies)
 function sanitizeSchemaStrings(obj: any): any {
   if (obj === null || obj === undefined) return obj
-  if (typeof obj === 'string') 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) ? [] : {}
+    const out: any = {}
     Object.keys(obj).forEach((k) => {
-      out[k] = sanitizeSchemaStrings(obj[k])
+      const v = (obj as any)[k]
+      if (k === 'css' && typeof v === 'string') {
+        out[k] = v
+      } else if (v && typeof v === 'object' && v.type === 'JSFunction' && typeof v.value === 'string') {
+        out[k] = v
+      } else {
+        out[k] = sanitizeSchemaStrings(v)
+      }
     })
     return out
   }
   return obj
 }
@@
-  // sanitize all strings to remove newlines in the final output
+  // sanitize (no-op for css/JSFunction)
   const sanitized = sanitizeSchemaStrings(schema)

Also applies to: 150-151

packages/vue-to-dsl/src/parsers/templateParser.ts (3)

140-143: Align v-if/v-show to JSExpression objects for consistency

Elsewhere you emit { type: 'JSExpression', value }. Do the same for schema.condition and v-show for uniform consumers.

-        schema.condition = prop.exp ? prop.exp.content : 'true'
+        schema.condition = { type: 'JSExpression', value: prop.exp ? prop.exp.content : 'true' }
@@
-        schema.props['v-show'] = prop.exp ? prop.exp.content : 'true'
+        schema.props['v-show'] = { type: 'JSExpression', value: prop.exp ? prop.exp.content : 'true' }

Also applies to: 155-157


148-153: Fix invalid/unsafe regex ([^]) in v-for parsing

[^] is a negated empty class and matches anything; Biome flags it. Replace with a safe any‑char class.

-          const match =
-            exp.match(/^[^]*?(?:\)|\S)\s+(?:in|of)\s+([^]+)$/) || exp.match(/^(?:[^]+?)\s+(?:in|of)\s+([^]+)$/)
+          const match =
+            exp.match(/^[\s\S]*?(?:\)|\S)\s+(?:in|of)\s+([\s\S]+)$/) ||
+            exp.match(/^(?:[\s\S]+?)\s+(?:in|of)\s+([\s\S]+)$/)

(Alternatively, slice by last " in " / " of " to avoid brittle regex.)


207-226: Icon normalization drops existing props/handlers

Overwriting schema.props loses classes, aria, and events parsed earlier (breaks behavior/accessibility). Preserve previous props and add name.

-  const styleVal = schema.props && typeof schema.props.style === 'string' ? schema.props.style : undefined
-  // Rebuild props: keep style if present; set name; drop other raw attributes like fill
-  schema.componentName = 'Icon'
-  schema.props = {}
-  if (styleVal) schema.props.style = styleVal
-  schema.props.name = iconName
+  const prev = { ...(schema.props || {}) }
+  schema.componentName = 'Icon'
+  schema.props = { ...prev, name: iconName }
+  // Optionally drop raw SVG-only attrs if needed:
+  // const { fill, stroke, ...rest } = schema.props; schema.props = { ...rest, name: iconName }
packages/vue-to-dsl/src/parsers/scriptParser.ts (1)

174-199: Generalize state extraction (ref/reactive) and run before method detection

Currently only variables literally named “state” are captured; typical const count = ref(1) is missed. Also, detect state/computed before methods. This prevents misclassification and captures Composition API patterns.

- function assignStateIfNamedState(name: string, init: any, result: any): boolean {
-  if (name !== 'state') return false
-  const initCode = getNodeValue(init)
-  if (isVueReactiveCall(init, 'reactive')) {
-    const firstArg = init.arguments && init.arguments[0]
-    if (t.isObjectExpression(firstArg)) {
-      firstArg.properties.forEach((prop: any) => {
-        if (t.isObjectProperty(prop) && t.isIdentifier(prop.key)) {
-          const propName = prop.key.name
-          const propValue = prop.value ? getNodeValue(prop.value) : undefined
-          result.state[propName] = { type: 'reactive', value: propValue }
-        }
-      })
-    } else {
-      result.state[name] = { type: 'reactive', value: initCode }
-    }
-    return true
-  }
-  if (isVueReactiveCall(init, 'ref')) {
-    result.state[name] = { type: 'ref', value: initCode }
-    return true
-  }
-  // normal non-reactive assignment to state
-  result.state[name] = { type: 'normal', value: initCode }
-  return true
-}
+function assignStateFromVueCalls(name: string, init: any, result: any): boolean {
+  const initCode = getNodeValue(init)
+  // reactive(...)
+  if (isVueReactiveCall(init, 'reactive')) {
+    const firstArg = init.arguments?.[0]
+    if (t.isObjectExpression(firstArg)) {
+      firstArg.properties.forEach((prop: any) => {
+        if (t.isObjectProperty(prop) && (t.isIdentifier(prop.key) || t.isStringLiteral(prop.key))) {
+          const propName = t.isIdentifier(prop.key) ? prop.key.name : prop.key.value
+          const propValue = prop.value ? getNodeValue(prop.value) : undefined
+          result.state[propName] = { type: 'reactive', value: propValue }
+        }
+      })
+    } else {
+      result.state[name] = { type: 'reactive', value: initCode }
+    }
+    return true
+  }
+  // ref(...)
+  if (isVueReactiveCall(init, 'ref')) {
+    result.state[name] = { type: 'ref', value: initCode }
+    return true
+  }
+  return false
+}
@@
-function handleVariableDeclarator(name: string, init: any, result: any, source: string) {
-  // 1) function-like assignments become methods
-  if (addMethodFromFunctionLike(name, init, result, source)) return
-  // 2) state-only extraction
-  if (assignStateIfNamedState(name, init, result)) return
-  // 3) computed regardless of name
-  if (assignComputedIfComputed(name, init, result, source)) return
-  // 4) otherwise ignored for state (per requirement), no-op
-}
+function handleVariableDeclarator(name: string, init: any, result: any, source: string) {
+  // 1) state/computed first
+  if (assignStateFromVueCalls(name, init, result)) return
+  if (assignComputedIfComputed(name, init, result, source)) return
+  // 2) then methods
+  if (addMethodFromFunctionLike(name, init, result, source)) return
+  // 3) ignore others
+}

Run to confirm no remaining references to the old helper and that ref/reactive vars are captured:

#!/bin/bash
rg -n "assignStateIfNamedState|assignStateFromVueCalls|ref\\(|reactive\\(" packages/vue-to-dsl -C2

Also applies to: 214-222

🧹 Nitpick comments (7)
packages/vue-to-dsl/test/testcases/001_simple/input/component.vue (1)

11-11: Remove redundant import of compiler macros.

In <script setup>, defineProps and defineEmits are compiler macros automatically available without import.

Apply this diff:

-import { defineProps, defineEmits } from 'vue'
packages/vue-to-dsl/test/testcases/004_dashboard/input/component.vue (1)

88-90: Unused handler go — keep intentionally or remove

go(item) is not referenced in the template. If it exists only to validate method extraction in tests, keep and add a brief comment; otherwise remove to avoid dead code.

packages/vue-to-dsl/test/testcases/005_survey/input/component.vue (2)

52-55: Injection may be null — add a defensive fallback

vue.inject(I18nInjectionKey) can be undefined if not provided. Guard to avoid runtime errors in isolated tests.

-const { t, lowcodeWrap, stores } = vue.inject(I18nInjectionKey).lowcode()
+const injected = vue.inject(I18nInjectionKey)
+const lowcode = injected?.lowcode?.()
+if (!lowcode) {
+  // Fallback for tests without provider
+  const noWrap = (x: any) => x
+  var t = (s: string) => s
+  var lowcodeWrap = (_: any, __: any) => (fn: any) => fn
+  var stores = {}
+} else {
+  var { t, lowcodeWrap, stores } = lowcode
+}

45-47: Minor: duplicate imports and unused t

You import the whole vue namespace and also named defineProps/defineEmits; you can drop the latter and reference from vue. Also t is declared but unused.

-import { defineProps, defineEmits } from 'vue'
+// use vue.defineProps / vue.defineEmits via the namespace import
packages/vue-to-dsl/src/generator/index.ts (1)

21-41: State conversion: keep non‑string reactive values as‑is

Current logic handles string primitives well. If reactive contains arrays/objects, convertToPlainValue returns them untouched — that’s OK. Add a short comment to document this intent for readers.

-        case 'reactive':
-          result[key] = convertToPlainValue(stateItem.value)
+        case 'reactive':
+          // If value is non-string (object/array), preserve as-is
+          result[key] = convertToPlainValue(stateItem.value)
packages/vue-to-dsl/src/parsers/scriptParser.ts (2)

48-89: Value conversion gaps (TemplateLiteral, Identifier, RegExp, BigInt)

Edge cases stringify to "undefined". Add common literal/expr support to reduce surprises.

 function getNodeValue(node: any): any {
+  if (t.isTemplateLiteral(node)) {
+    const cooked = node.quasis.map((q) => q.value.cooked ?? '').join('${}')
+    return '`' + cooked + '`'
+  }
+  if (t.isIdentifier(node)) return node.name
+  if ((t as any).isBigIntLiteral?.(node)) return String((node as any).value)
+  if (t.isRegExpLiteral(node)) return `/${node.pattern}/${node.flags}`
   if (t.isStringLiteral(node)) return node.value
   if (t.isNumericLiteral(node)) return node.value

460-463: Broaden parser plugins for robustness, but apply consistently across parsers

The suggestion to add topLevelAwait and importMeta plugins is technically sound—these are standard Babel parser plugins that would help handle Vue projects using these ES/TS features. However, verification reveals two issues:

  1. No current usage or failures: These features aren't used anywhere in the codebase, and no parse failures are reported by existing integration tests.

  2. Inconsistency: The identical parser configuration (plugins: ['typescript', 'jsx']) appears in both scriptParser.ts and templateParser.ts (line 124). If parseScript is broadened, templateParser should be updated similarly for consistency.

If you proceed with this optional enhancement, apply it to both parsers and consider adding test cases that verify the parser handles these features correctly.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 43ebd58 and 504530a.

📒 Files selected for processing (14)
  • packages/vue-to-dsl/src/generator/index.ts (1 hunks)
  • packages/vue-to-dsl/src/parsers/scriptParser.ts (1 hunks)
  • packages/vue-to-dsl/src/parsers/templateParser.ts (1 hunks)
  • packages/vue-to-dsl/test/testcases/001_simple/expected/schema.json (1 hunks)
  • packages/vue-to-dsl/test/testcases/001_simple/input/component.vue (1 hunks)
  • packages/vue-to-dsl/test/testcases/002_createVM/expected/schema.json (1 hunks)
  • packages/vue-to-dsl/test/testcases/002_createVM/input/component.vue (1 hunks)
  • packages/vue-to-dsl/test/testcases/003_login/expected/schema.json (1 hunks)
  • packages/vue-to-dsl/test/testcases/003_login/input/component.vue (1 hunks)
  • packages/vue-to-dsl/test/testcases/004_dashboard/expected/schema.json (1 hunks)
  • packages/vue-to-dsl/test/testcases/004_dashboard/input/component.vue (1 hunks)
  • packages/vue-to-dsl/test/testcases/005_survey/expected/schema.json (1 hunks)
  • packages/vue-to-dsl/test/testcases/005_survey/input/component.vue (1 hunks)
  • packages/vue-to-dsl/test/testcases/index.test.js (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • packages/vue-to-dsl/test/testcases/index.test.js
🧰 Additional context used
🧬 Code graph analysis (3)
packages/vue-to-dsl/src/generator/index.ts (3)
packages/vue-generator/src/generator/vue/sfc/generateAttribute.js (1)
  • props (596-596)
packages/vue-to-dsl/src/index.ts (2)
  • generateSchema (5-5)
  • generateAppSchema (5-5)
packages/vue-to-dsl/src/constants.ts (1)
  • defaultComponentsMap (18-217)
packages/vue-to-dsl/src/parsers/scriptParser.ts (2)
packages/vue-to-dsl/src/index.ts (1)
  • parseScript (6-6)
packages/vue-to-dsl/src/parsers/index.ts (1)
  • parseScript (2-2)
packages/vue-to-dsl/src/parsers/templateParser.ts (5)
packages/vue-generator/src/generator/vue/sfc/generateAttribute.js (1)
  • props (596-596)
packages/vue-generator/src/generator/vue/sfc/generateTemplate.js (1)
  • name (40-40)
packages/vue-generator/src/templates/vue-template/index.js (1)
  • value (30-30)
packages/vue-to-dsl/src/index.ts (1)
  • parseTemplate (6-6)
packages/vue-to-dsl/src/parsers/index.ts (1)
  • parseTemplate (1-1)
🪛 Biome (2.1.2)
packages/vue-to-dsl/src/parsers/templateParser.ts

[error] 149-149: The regular expression includes this negated empty character class.

Negated empty character classes match anything.
If you want to match against [, escape it [.
Otherwise, remove the character class or fill it.

(lint/correctness/noEmptyCharacterClassInRegex)


[error] 149-149: The regular expression includes this negated empty character class.

Negated empty character classes match anything.
If you want to match against [, escape it [.
Otherwise, remove the character class or fill it.

(lint/correctness/noEmptyCharacterClassInRegex)


[error] 149-149: The regular expression includes this negated empty character class.

Negated empty character classes match anything.
If you want to match against [, escape it [.
Otherwise, remove the character class or fill it.

(lint/correctness/noEmptyCharacterClassInRegex)


[error] 149-149: The regular expression includes this negated empty character class.

Negated empty character classes match anything.
If you want to match against [, escape it [.
Otherwise, remove the character class or fill it.

(lint/correctness/noEmptyCharacterClassInRegex)

🔇 Additional comments (8)
packages/vue-to-dsl/test/testcases/001_simple/input/component.vue (1)

17-34: Test fixture patterns are appropriate for converter validation.

The lowcodeWrap pattern, nested setup function, and immediate invocation are non-idiomatic Vue but appear intentional for testing the DSL converter's ability to handle various code structures and extract lifecycle hooks, state, and methods correctly.

packages/vue-to-dsl/test/testcases/001_simple/expected/schema.json (1)

1-60: Expected schema correctly represents the DSL conversion.

The schema accurately captures the Vue SFC structure:

  • Initial state (switchStatus: true) from reactive declaration
  • Methods (onClickNew) with correct toggle logic
  • Lifecycle (setup) preserving console.log and state mutation
  • CSS extracted from scoped styles
  • Template tree with proper v-model → modelValue and @click → onClick conversions
  • Component IDs ensure stable references for test validation
packages/vue-to-dsl/test/testcases/005_survey/expected/schema.json (1)

1-223: LGTM — schema matches expected DSL shape

Bindings, JSFunction formatting, and ids look coherent with generator output.

packages/vue-to-dsl/test/testcases/004_dashboard/expected/schema.json (1)

1-408: LGTM — expected dashboard schema is consistent

Props casing, JSExpression usage, and layout tree align with parser/generator conventions.

packages/vue-to-dsl/src/generator/index.ts (2)

133-155: Public API composition looks good

Flow from script/style/template into the Page schema is clear and modular.


157-170: App schema defaults are sensible

Default i18n/utils/dataSource/globalState and components map wiring look fine.

packages/vue-to-dsl/src/parsers/templateParser.ts (1)

228-246: Parser structure and node recursion look solid

General element/text/interpolation handling is clear and idiomatic.

packages/vue-to-dsl/src/parsers/scriptParser.ts (1)

344-371: I need to gather more information about the current implementation and test expectations.

Now let me look at the test file to see what assertions it makes:

Now let me search the codebase for the test assertions and current implementation:

Let me run a shell script to inspect the actual codebase:Based on my web searches, I have not found specific information about the vue-to-dsl codebase. Let me now generate the final rewritten review comment based on the information gathered and the context from the test output I initially received.

Extract actual state properties from Options API data() function

The current implementation uses a placeholder string and loses the actual state properties. The proposed diff correctly extracts the returned object from data() and populates result.state with the actual properties using Babel AST traversal.

However, verification requires confirming:

  1. The getNodeValue() helper function exists in the codebase and correctly extracts literal values
  2. The test assertions in converter.test.js actually validate that schema.state.message === 'Hello from Options API' (and similar state properties)
  3. The implementation handles edge cases like arrow functions, nested properties, and computed values

These details could not be confirmed from available codebase context.

Comment on lines +102 to +116
// 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)
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

IDs are non‑deterministic — make them stable across runs

generateId() uses Math.random(), violating the PR goal of “stable references” and making snapshot tests flaky. Derive ids from traversal path to be deterministic.

+import { createHash } from 'crypto'
@@
-// 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)
-}
+// Generate a deterministic 8-char id from a stable input (path + componentName)
+function generateIdFrom(input: string): string {
+  return createHash('md5').update(input).digest('hex').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)
-}
+// Recursively assign deterministic ids using preorder path
+function assignComponentIds(node: any, path = '0'): void {
+  if (!node || typeof node !== 'object') return
+  if (typeof node.componentName === 'string') {
+    if (!node.id) node.id = generateIdFrom(`${path}:${node.componentName}`)
+  }
+  if (Array.isArray(node.children)) {
+    node.children.forEach((child: any, idx: number) => assignComponentIds(child, `${path}.${idx}`))
+  }
+}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// 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)
}
import { createHash } from 'crypto'
// Generate a deterministic 8-char id from a stable input (path + componentName)
function generateIdFrom(input: string): string {
return createHash('md5').update(input).digest('hex').slice(0, 8)
}
// Recursively assign deterministic ids using preorder path
function assignComponentIds(node: any, path = '0'): void {
if (!node || typeof node !== 'object') return
if (typeof node.componentName === 'string') {
if (!node.id) node.id = generateIdFrom(`${path}:${node.componentName}`)
}
if (Array.isArray(node.children)) {
node.children.forEach((child: any, idx: number) => assignComponentIds(child, `${path}.${idx}`))
}
}

Comment on lines +260 to +272
if (t.isReturnStatement(statement) && t.isObjectExpression(statement.argument)) {
statement.argument.properties.forEach((prop: any) => {
if (t.isObjectProperty(prop) && t.isIdentifier(prop.key)) {
const name = prop.key.name
if (!result.state[name] && !result.computed[name] && declaredFunctions.has(name)) {
const bodyCode = functionBodies[name] || `function ${name}(){}`
result.methods[name] = { type: 'function', value: bodyCode }
} else if (!result.state[name] && !result.methods[name] && !result.computed[name]) {
result.methods[name] = { type: 'function', value: `function ${name}(){}` }
}
}
})
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Don’t invent methods for exposed non-function bindings in setup return

Returning count or any ref from setup() becomes a fake method stub. That’s incorrect and breaks DSL fidelity.

-        if (t.isObjectProperty(prop) && t.isIdentifier(prop.key)) {
+        if (t.isObjectProperty(prop) && t.isIdentifier(prop.key)) {
           const name = prop.key.name
-          if (!result.state[name] && !result.computed[name] && declaredFunctions.has(name)) {
+          if (declaredFunctions.has(name)) {
             const bodyCode = functionBodies[name] || `function ${name}(){}`
             result.methods[name] = { type: 'function', value: bodyCode }
-          } else if (!result.state[name] && !result.methods[name] && !result.computed[name]) {
-            result.methods[name] = { type: 'function', value: `function ${name}(){}` }
           }
         }

Follow-up: once state extraction is generalized (previous comment), refs/reactives will already be present in result.state; unknown exposures should be left untouched rather than coerced into methods.

🤖 Prompt for AI Agents
In packages/vue-to-dsl/src/parsers/scriptParser.ts around lines 260 to 272 the
code currently invents method stubs for any name returned from setup() that
isn't in result.state or result.computed, producing fake methods for
refs/reactives; change the logic so you only add an entry to result.methods when
the returned name is a known function (e.g. declaredFunctions.has(name) or a
corresponding functionBodies[name] exists); remove the branch that
unconditionally creates a `function name(){}` stub for unknown exposures so
unknown/non-function bindings are left untouched.

Comment on lines +406 to +411
<style scoped>
body {
background-color: #eef0f5;
margin-bottom: 80px;
}
</style>
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Scoped styles won't apply to body element.

The <style scoped> block targets body, which is outside the component's scope. Scoped styles only apply to elements within the component template.

Remove the scoped attribute or move body styles to a global stylesheet.

🤖 Prompt for AI Agents
In packages/vue-to-dsl/test/testcases/002_createVM/input/component.vue around
lines 406-411, the <style scoped> block is styling the global body element which
won't be affected because scoped styles only apply to elements inside the
component; either remove the scoped attribute so the body rules become global,
or move the body styles into a global stylesheet (e.g., main.css or a
root/global <style> without scoped) and keep component-specific styles scoped.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

♻️ Duplicate comments (10)
packages/vue-to-dsl/test/testcases/002_createVM/expected/schema.json (2)

61-67: Duplicate/colliding props: emit one canonical casing (prefer camelCase)

Both labelWidth/labelPosition and label-width/label-position are emitted. Keep one form; recommend camelCase and dedupe by explicit precedence (kebab → camel). Apply across all similar blocks in this file.

Example fix (apply pattern to all occurrences):

-                "labelWidth": "80px",
-                "labelPosition": "top",
-                "label-position": "left ",
-                "label-width": "150px",
+                "labelWidth": "150px",
+                "labelPosition": "left",

Also update the converter/generator to normalize prop casing and resolve conflicts deterministically (last-write-wins with kebab → camel precedence).

Also applies to: 199-205, 538-544, 667-673, 746-752, 907-913


118-122: Invalid CSS serialization: "[object Event]" leaked into style

"background-color: [object Event]" indicates a non-primitive slipped into CSS serialization. Guard during style stringify to only include string/number values; skip others. Update generator and fixtures.

Minimal fixture correction:

-                        "style": "background-color: [object Event]; color: #8a8e99; font-size: 12px"
+                        "style": "color: #8a8e99; font-size: 12px"

Generator guard (pseudo-TypeScript):

for (const [k, v] of Object.entries(styleObj)) {
  if (typeof v !== 'string' && typeof v !== 'number') continue
  parts.push(`${toKebab(k)}: ${String(v)}`)
}
packages/vue-to-dsl/src/parsers/scriptParser.ts (2)

174-199: Generalize state extraction and reorder detection to prevent misclassification

Current logic only extracts when variable is literally state and runs after method detection, missing const count = ref(1) and potentially misclassifying.

Apply:

- function assignStateIfNamedState(name: string, init: any, result: any): boolean {
-   if (name !== 'state') return false
-   const initCode = getNodeValue(init)
-   if (isVueReactiveCall(init, 'reactive')) {
-     const firstArg = init.arguments && init.arguments[0]
-     if (t.isObjectExpression(firstArg)) {
-       firstArg.properties.forEach((prop: any) => {
-         if (t.isObjectProperty(prop) && t.isIdentifier(prop.key)) {
-           const propName = prop.key.name
-           const propValue = prop.value ? getNodeValue(prop.value) : undefined
-           result.state[propName] = { type: 'reactive', value: propValue }
-         }
-       })
-     } else {
-       result.state[name] = { type: 'reactive', value: initCode }
-     }
-     return true
-   }
-   if (isVueReactiveCall(init, 'ref')) {
-     result.state[name] = { type: 'ref', value: initCode }
-     return true
-   }
-   // normal non-reactive assignment to state
-   result.state[name] = { type: 'normal', value: initCode }
-   return true
- }
+ function assignStateFromVueCalls(name: string, init: any, result: any): boolean {
+   const initCode = getNodeValue(init)
+   // reactive(...)
+   if (isVueReactiveCall(init, 'reactive')) {
+     const firstArg = init.arguments?.[0]
+     if (t.isObjectExpression(firstArg)) {
+       firstArg.properties.forEach((prop: any) => {
+         if (t.isObjectProperty(prop) && (t.isIdentifier(prop.key) || t.isStringLiteral(prop.key))) {
+           const propName = t.isIdentifier(prop.key) ? prop.key.name : prop.key.value
+           const propValue = prop.value ? getNodeValue(prop.value) : undefined
+           result.state[propName] = { type: 'reactive', value: propValue }
+         }
+       })
+     } else {
+       result.state[name] = { type: 'reactive', value: initCode }
+     }
+     return true
+   }
+   // ref(...)
+   if (isVueReactiveCall(init, 'ref')) {
+     result.state[name] = { type: 'ref', value: initCode }
+     return true
+   }
+   return false
+ }
@@
- function handleVariableDeclarator(name: string, init: any, result: any, source: string) {
-   // 1) function-like assignments become methods
-   if (addMethodFromFunctionLike(name, init, result, source)) return
-   // 2) state-only extraction
-   if (assignStateIfNamedState(name, init, result)) return
-   // 3) computed regardless of name
-   if (assignComputedIfComputed(name, init, result, source)) return
-   // 4) otherwise ignored for state (per requirement), no-op
- }
+ function handleVariableDeclarator(name: string, init: any, result: any, source: string) {
+   // 1) state/computed first to avoid misclassification
+   if (assignStateFromVueCalls(name, init, result)) return
+   if (assignComputedIfComputed(name, init, result, source)) return
+   // 2) then function-like → methods/lifecycle
+   if (addMethodFromFunctionLike(name, init, result, source)) return
+ }

Also applies to: 214-222


260-272: Don’t invent methods for non-function bindings returned from setup()

Returning refs/reactives currently creates fake method stubs. Only register a method when it’s a known declared function.

Apply:

-          if (!result.state[name] && !result.computed[name] && declaredFunctions.has(name)) {
+          if (!result.state[name] && !result.computed[name] && declaredFunctions.has(name)) {
             const bodyCode = functionBodies[name] || `function ${name}(){}`
             result.methods[name] = { type: 'function', value: bodyCode }
-          } else if (!result.state[name] && !result.methods[name] && !result.computed[name]) {
-            result.methods[name] = { type: 'function', value: `function ${name}(){}` }
           }
packages/vue-to-dsl/src/index.d.ts (5)

17-27: TemplateParser return type should be an array
Implementation returns TemplateSchema[].

 export interface TemplateParser {
-  parse(template: string, options?: any): TemplateSchema
+  parse(template: string, options?: any): TemplateSchema[]
 }
 
 export interface ScriptParser {
   parse(script: string, options?: any): ScriptSchema
 }
 
 export interface StyleParser {
   parse(style: string, options?: any): StyleSchema
 }

40-48: Expose parser soft-failure signal on ScriptSchema

Include optional error field to mirror parser behavior.

 export interface ScriptSchema {
   state?: Record<string, any>
   methods?: Record<string, any>
   computed?: Record<string, any>
   lifeCycles?: Record<string, any>
   imports?: ImportInfo[]
   props?: PropInfo[]
   emits?: string[]
+  error?: string
 }

69-81: Make PageSchema.path optional

Generator doesn’t always set path.

 export interface PageSchema {
   componentName: 'Page'
   fileName: string
-  path: string
+  path?: string
   meta?: Record<string, any>
   state?: Record<string, any>
   methods?: Record<string, any>
   computed?: Record<string, any>
   lifeCycles?: Record<string, any>
   props?: PropInfo[]
   css?: string
   children?: TemplateSchema[]
 }

83-88: Allow ConvertResult.schema to be null on failure

Matches converter’s error path.

 export interface ConvertResult {
-  schema: PageSchema
+  schema: PageSchema | null
   dependencies: string[]
   errors: string[]
   warnings: string[]
 }

132-137: Fix generateSchema signature to match implementation

Parameters are parsed artifacts, not raw strings.

 export function generateSchema(
-  template: string,
-  script: string,
-  style?: string,
+  template: TemplateSchema[],
+  script: ScriptSchema,
+  style?: StyleSchema,
   options?: VueToSchemaOptions
 ): Promise<PageSchema>
packages/vue-to-dsl/src/generator/index.ts (1)

102-116: Make component IDs deterministic (no Math.random)
Current IDs violate “stable references” goal and make snapshots flaky.

Apply deterministic hashing from traversal path + componentName.

+import { createHash } from 'crypto'
@@
-// 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)
-}
+// Generate a deterministic 8-char id from a stable input
+function generateIdFrom(input: string): string {
+  return createHash('md5').update(input).digest('hex').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)
-}
+// Recursively assign deterministic ids using preorder path
+function assignComponentIds(node: any, path = '0'): void {
+  if (!node || typeof node !== 'object') return
+  if (typeof node.componentName === 'string' && !node.id) {
+    node.id = generateIdFrom(`${path}:${node.componentName}`)
+  }
+  if (Array.isArray(node.children)) {
+    node.children.forEach((child: any, idx: number) => assignComponentIds(child, `${path}.${idx}`))
+  }
+}
🧹 Nitpick comments (10)
packages/vue-to-dsl/test/sfc/converter.test.js (1)

82-105: Add tests for standalone ref and avoiding fake method stubs

Strengthen coverage to (1) capture standalone ref as state and (2) ensure returned refs aren’t turned into method stubs.

Append:

+  it('should capture standalone ref as state', async () => {
+    const vueCode = `
+<script setup>
+import { ref } from 'vue'
+const count = ref(1)
+</script>`
+    const result = await converter.convertFromString(vueCode)
+    expect(result.errors).toHaveLength(0)
+    expect(result.schema.state.count).toBeDefined()
+  })
+
+  it('should NOT create fake methods for returned refs', async () => {
+    const vueCode = `
+<script setup>
+import { ref } from 'vue'
+const count = ref(1)
+export default {}
+</script>`
+    const result = await converter.convertFromString(vueCode)
+    expect(result.errors).toHaveLength(0)
+    expect(result.schema.methods.count).toBeUndefined()
+  })
packages/vue-to-dsl/src/parsers/scriptParser.ts (1)

347-355: Optional: parse Options API data() to extract initial state

Currently sets a placeholder function string. Consider parsing the data() return object to populate result.state keys for better fidelity.

packages/vue-to-dsl/test/full/expected/schema.json (3)

1226-1246: Normalize v-model → modelValue consistently

DSL currently mixes "modelValue" and "v-model". Use one convention (typically "modelValue" with events) to avoid renderer ambiguity.

If the runtime expects "modelValue", rewrite "v-model" props during generation.

Also applies to: 1279-1296, 1335-1352, 1380-1396, 1423-1430, 1650-1666, 1670-1686, 1756-1773, 1776-1783, 1864-1871, 1890-1897, 2025-2033


1899-1903: Inconsistent expression context in loop

Uses "this.state.dataDisk" while other bindings use "state.*". Pick one convention; recommend dropping "this." for consistency with other JSExpressions.

Apply during template transform when emitting loop.value.


674-676: Avoid external localhost URLs in test fixtures

Hard-coded http://localhost:9090 couples tests to an external service.

Mock or replace with inert placeholder and assert shape, not fetch result.

packages/vue-to-dsl/src/generator/index.ts (3)

118-131: Sanitizer is a no-op but comment claims sanitization

Function returns strings unchanged; the name/comment mislead.

Either implement real sanitization (and skip css/JSFunction) or remove the pass and update the comment to avoid confusion.


2-11: convertToPlainValue: safer string de-quoting

Regex ^['"].*['"]$ is greedy and ignores escapes; can mangle edge cases.

Prefer precise match on same-quote ends and basic escape handling:

-  if (/^['"].*['"]$/.test(trimmed)) return trimmed.slice(1, -1)
+  if ((trimmed.startsWith('"') && trimmed.endsWith('"')) || (trimmed.startsWith("'") && trimmed.endsWith("'"))) {
+    const s = trimmed.slice(1, -1)
+    return s.replace(/\\(["'\\nrtb])/g, (_m, c) => ({ n: '\n', r: '\r', t: '\t', b: '\b', '"': '"', "'": "'", '\\': '\\' }[c] ?? c))
+  }

157-170: Optional: enrich app meta and stamp

Fixture includes generatedAt/generator fields. Consider populating them here to centralize behavior.

E.g., set meta.generatedAt = new Date().toISOString() and meta.generator with package name; gate behind option to keep tests deterministic.

packages/vue-to-dsl/src/index.d.ts (2)

2-15: Options type lacks fields used by implementation

Add common generation fields to VueToSchemaOptions.

 export interface VueToSchemaOptions {
   componentMap?: Record<string, string>
   preserveComments?: boolean
   strictMode?: boolean
   customParsers?: {
     template?: TemplateParser
     script?: ScriptParser
     style?: StyleParser
   }
+  fileName?: string
+  path?: string
+  title?: string
+  description?: string
 }

112-118: Broaden parseVueFile/parseSFC return metadata

Expose langs, style/custom blocks, and warnings for consumers.

 export function parseVueFile(filePath: string): Promise<{
   template?: string
   script?: string
   style?: string
   scriptSetup?: string
+  templateLang?: string
+  scriptLang?: string
+  scriptSetupLang?: string
+  styleBlocks?: Array<{ content: string; lang?: string; scoped?: boolean; module?: boolean }>
+  customBlocks?: Array<{ type: string; content: string; attrs: Record<string, any> }>
+  warnings?: any[]
 }>
 
 export function parseSFC(vueCode: string): {
   template?: string
   script?: string
   style?: string
   scriptSetup?: string
+  templateLang?: string
+  scriptLang?: string
+  scriptSetupLang?: string
+  styleBlocks?: Array<{ content: string; lang?: string; scoped?: boolean; module?: boolean }>
+  customBlocks?: Array<{ type: string; content: string; attrs: Record<string, any> }>
+  warnings?: any[]
 }

Also applies to: 122-128

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 504530a and b5d457e.

📒 Files selected for processing (15)
  • packages/vue-to-dsl/cli.ts (1 hunks)
  • packages/vue-to-dsl/src/generator/index.ts (1 hunks)
  • packages/vue-to-dsl/src/index.d.ts (1 hunks)
  • packages/vue-to-dsl/src/parsers/scriptParser.ts (1 hunks)
  • packages/vue-to-dsl/src/types/index.js (1 hunks)
  • packages/vue-to-dsl/test/full/expected/schema.from-zip.json (1 hunks)
  • packages/vue-to-dsl/test/full/expected/schema.json (1 hunks)
  • packages/vue-to-dsl/test/sfc/converter.test.js (1 hunks)
  • packages/vue-to-dsl/test/testcases/001_simple/expected/schema.json (1 hunks)
  • packages/vue-to-dsl/test/testcases/002_createVM/expected/schema.json (1 hunks)
  • packages/vue-to-dsl/test/testcases/003_login/expected/schema.json (1 hunks)
  • packages/vue-to-dsl/test/testcases/004_dashboard/expected/schema.json (1 hunks)
  • packages/vue-to-dsl/test/testcases/005_survey/expected/schema.json (1 hunks)
  • packages/vue-to-dsl/test/testcases/006_lifecycle/expected/schema.json (1 hunks)
  • packages/vue-to-dsl/test/testcases/006_lifecycle/input/component.vue (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (7)
  • packages/vue-to-dsl/test/testcases/001_simple/expected/schema.json
  • packages/vue-to-dsl/test/full/expected/schema.from-zip.json
  • packages/vue-to-dsl/cli.ts
  • packages/vue-to-dsl/test/testcases/004_dashboard/expected/schema.json
  • packages/vue-to-dsl/test/testcases/003_login/expected/schema.json
  • packages/vue-to-dsl/test/testcases/005_survey/expected/schema.json
  • packages/vue-to-dsl/src/types/index.js
🧰 Additional context used
🧬 Code graph analysis (4)
packages/vue-to-dsl/test/sfc/converter.test.js (2)
packages/vue-to-dsl/test/testcases/index.test.js (1)
  • converter (8-8)
packages/vue-to-dsl/src/converter.ts (1)
  • VueToDslConverter (34-822)
packages/vue-to-dsl/src/generator/index.ts (2)
packages/vue-to-dsl/src/index.ts (2)
  • generateSchema (5-5)
  • generateAppSchema (5-5)
packages/vue-to-dsl/src/constants.ts (1)
  • defaultComponentsMap (18-217)
packages/vue-to-dsl/src/index.d.ts (2)
packages/vue-to-dsl/src/generator/index.ts (1)
  • generateSchema (133-155)
packages/vue-to-dsl/src/parsers/scriptParser.ts (1)
  • parseScript (460-488)
packages/vue-to-dsl/src/parsers/scriptParser.ts (2)
packages/vue-to-dsl/src/index.ts (1)
  • parseScript (6-6)
packages/vue-to-dsl/src/parsers/index.ts (1)
  • parseScript (2-2)
🔇 Additional comments (3)
packages/vue-to-dsl/test/testcases/006_lifecycle/input/component.vue (1)

5-48: LGTM for lifecycle fixture

Covers the major hooks cleanly for the converter to map into DSL. No changes needed.

packages/vue-to-dsl/test/testcases/006_lifecycle/expected/schema.json (1)

10-43: Confirm canonical value type for lifeCycles

Parser writes type: 'lifecycle' while expected uses type: 'JSFunction'. Ensure generator consistently normalizes to a single canonical type to avoid fixture drift.

packages/vue-to-dsl/src/parsers/scriptParser.ts (1)

124-131: Align lifecycle entry type with downstream schema

setLifecycleEntry uses type: 'lifecycle'. If DSL canonical type is JSFunction, update here or in the generator to maintain consistency.

Comment on lines +139 to +153
/**
* 解析模板
*/
export function parseTemplate(template: string): TemplateSchema[]

/**
* 解析脚本
*/
export function parseScript(script: string): ScriptSchema

/**
* 解析样式
*/
export function parseStyle(style: string): StyleSchema
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

Export generateAppSchema in the public API

Index.ts re-exports it; add typing here.

 export function parseStyle(style: string): StyleSchema
+
+/**
+ * 生成应用级 DSL Schema(App)
+ */
+export function generateAppSchema(pageSchemas: PageSchema[], options?: any): any

🏁 Script executed:

#!/bin/bash
# Ensure index.ts exports generateAppSchema and consumers import it
rg -nP 'generateAppSchema' -C2 packages/vue-to-dsl/src/index.ts packages/vue-to-dsl/**/cli.* packages/vue-to-dsl/**/test/**.*

Length of output: 575


The search results are about generic .d.ts generation, not the specific codebase. Let me verify whether generateAppSchema is already typed in index.d.ts.


---

🏁 Script executed:

```shell
#!/bin/bash
# Search for generateAppSchema in index.d.ts file
rg -n 'generateAppSchema' packages/vue-to-dsl/src/index.d.ts

Length of output: 74


Let me search for all .d.ts files and locate where types are actually defined:


---

🏁 Script executed:

```shell
#!/bin/bash
# Find all .d.ts files in packages/vue-to-dsl
fd -e d.ts packages/vue-to-dsl/

# Also search for PageSchema definition
rg -n 'PageSchema' packages/vue-to-dsl/src/ -g '*.ts' -C1

Length of output: 939


Good! Now let me verify the actual implementation of generateAppSchema and check the full context of index.d.ts:


---

🏁 Script executed:

```shell
#!/bin/bash
# Search for generateAppSchema implementation
rg -n 'generateAppSchema' packages/vue-to-dsl/src/generator.ts -A5

# Get the full export section from index.d.ts
cat packages/vue-to-dsl/src/index.d.ts | tail -30

Length of output: 585


Let me search for where the generator module is located and find the generateAppSchema signature:


---

🏁 Script executed:

```shell
#!/bin/bash
# Find generator files
fd -e ts -e js . packages/vue-to-dsl/src/ | grep -i generator

# Search for the generateAppSchema function definition
rg -n 'export.*generateAppSchema|function generateAppSchema' packages/vue-to-dsl/src/ -A3

Length of output: 642


Add generateAppSchema typing to index.d.ts public API

The function is exported from index.ts but lacks type definitions. Add the missing export:

  export function parseStyle(style: string): StyleSchema
+
+  /**
+   * 生成应用级 DSL Schema(App)
+   */
+  export function generateAppSchema(pageSchemas: PageSchema[], options?: any): any
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
/**
* 解析模板
*/
export function parseTemplate(template: string): TemplateSchema[]
/**
* 解析脚本
*/
export function parseScript(script: string): ScriptSchema
/**
* 解析样式
*/
export function parseStyle(style: string): StyleSchema
}
/**
* 解析模板
*/
export function parseTemplate(template: string): TemplateSchema[]
/**
* 解析脚本
*/
export function parseScript(script: string): ScriptSchema
/**
* 解析样式
*/
export function parseStyle(style: string): StyleSchema
/**
* 生成应用级 DSL Schema(App)
*/
export function generateAppSchema(pageSchemas: PageSchema[], options?: any): any
}
🤖 Prompt for AI Agents
In packages/vue-to-dsl/src/index.d.ts around lines 139 to 153, add the missing
public typing for the exported generateAppSchema function so the declaration
file matches index.ts; declare and export a function signature such as export
function generateAppSchema(source: string): AppSchema (or the correct
parameter/return types used in index.ts) and ensure the AppSchema type is
imported or declared and exported from the d.ts file so consumers can use the
typed API.

Comment on lines +1184 to +1196
"componentName": "span",
"props": {
"style": "background-color: [object Event]; color: #8a8e99; font-size: 12px"
},
"children": [
{
"componentName": "Text",
"props": {
"text": "温馨提示:页面左上角切换区域"
},
"id": "w6zeqr7n"
}
],
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Suspicious inline style contains “[object Event]”

The style value suggests an event object was stringified during conversion. Likely a bug in style binding serialization.

Please ensure style props are either plain strings or JSExpression values; avoid passing event objects through string coercion. Example upstream fix: when normalizing style bindings, only stringify literals; wrap dynamic expressions as { type: 'JSExpression', value: '<expr>' }.

🤖 Prompt for AI Agents
In packages/vue-to-dsl/test/full/expected/schema.json around lines 1184 to 1196,
the inline style for the span was stringified to include “[object Event]”
indicating an event object was coerced into the style string; update the style
normalization so it does not stringify Event/Function objects — detect
non-primitive style values and instead emit either a plain CSS string for static
styles or a JSExpression wrapper for dynamic expressions (e.g., { type:
'JSExpression', value: '<expr>' }); ensure any object values that are not valid
CSS maps are filtered out or converted to expression nodes and add a unit test
to assert style props are either strings or JSExpression objects, not
stringified events.

Comment on lines +2194 to +2483
{
"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
}
]
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

Icon component used but not declared in componentsMap

Schema references componentName "Icon" (e.g., Lines 1834–1841, 1912–1919) but componentsMap lacks a corresponding entry. Missing mapping will break runtime resolution.

Add an "Icon" mapping (or alias) to componentsMap or defaultComponentsMap.


🏁 Script executed:

#!/bin/bash
# Check for Icon usage vs mapping
rg -nP '"componentName":\s*"Icon"' -C2 packages/vue-to-dsl/test/full/expected/schema.json
rg -nP '"componentName":\s*"Icon"' -C2
rg -nP '"componentName":\s*"Icon"|Icon' -C2 packages/vue-to-dsl/src/constants.ts

Length of output: 211008


Icon component missing from componentsMap

The Icon component is referenced in the schema (lines 1834, 1912) but has no corresponding entry in the componentsMap (lines 2194-2483). This will cause runtime resolution failures.

Add an Icon mapping entry following the existing pattern:

{
  "componentName": "Icon",
  "package": "@opentiny/vue",
  "exportName": "Icon",
  "destructuring": true,
  "version": "<appropriate_version>"
}
🤖 Prompt for AI Agents
In packages/vue-to-dsl/test/full/expected/schema.json around lines 2194 to 2483,
the componentsMap array is missing an entry for the Icon component referenced
earlier, causing runtime resolution failures; add a new object to the array
matching the existing entries, e.g. with "componentName": "Icon", "package":
"@opentiny/vue", "exportName": "Icon", "destructuring": true and set "version"
to the appropriate package version (consistent with other @opentiny/vue
components, e.g. "3.24.0"), placed in the same format as the surrounding
component entries.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

♻️ Duplicate comments (3)
packages/vue-to-dsl/src/parsers/templateParser.ts (3)

152-154: Standardize v-if/v-show to JSExpression objects (consistent schema shape)

Elsewhere expressions are wrapped; here they’re raw strings. Wrap and prefix for consistency.

Apply:

-        schema.condition = prop.exp ? prop.exp.content : 'true'
+        schema.condition = { type: 'JSExpression', value: ensureThisPrefix(prop.exp ? prop.exp.content : 'true') }
...
-        schema.props['v-show'] = prop.exp ? prop.exp.content : 'true'
+        schema.props['v-show'] = { type: 'JSExpression', value: ensureThisPrefix(prop.exp ? prop.exp.content : 'true') }
...
-        schema.props[`v-${directiveName}`] = prop.exp ? prop.exp.content : 'true'
+        {
+          const raw = prop.exp ? prop.exp.content : 'true'
+          const parsed = parseLiteralExpression(raw)
+          schema.props[`v-${directiveName}`] = parsed.ok
+            ? parsed.value
+            : { type: 'JSExpression', value: ensureThisPrefix(raw) }
+        }

Also applies to: 166-168, 204-206


160-164: Fix invalid [^] regex; avoid linter error and overmatching

Replace negated empty character classes with a safer approach. Simpler and clearer: slice after the last " in " or " of ".

Apply:

-          const match =
-            exp.match(/^[^]*?(?:\)|\S)\s+(?:in|of)\s+([^]+)$/) || exp.match(/^(?:[^]+?)\s+(?:in|of)\s+([^]+)$/)
-          const src = (match ? match[1] : exp).trim()
+          const inIdx = exp.lastIndexOf(' in ')
+          const ofIdx = exp.lastIndexOf(' of ')
+          const idx = Math.max(inIdx, ofIdx)
+          const src = (idx >= 0 ? exp.slice(idx + 4) : exp).trim()
           schema.loop = { type: 'JSExpression', value: ensureThisPrefix(src) }

240-245: Icon normalization drops existing props/handlers

Reassigning props loses classes, aria, and events parsed earlier. Preserve and extend instead.

Apply:

-  const styleVal = schema.props && typeof schema.props.style === 'string' ? schema.props.style : undefined
-  // Rebuild props: keep style if present; set name; drop other raw attributes like fill
-  schema.componentName = 'Icon'
-  schema.props = {}
-  if (styleVal) schema.props.style = styleVal
-  schema.props.name = iconName
+  const prev = { ...(schema.props || {}) }
+  schema.componentName = 'Icon'
+  schema.props = { ...prev, name: iconName }
+  // If needed, selectively strip SVG-only attrs here instead of nuking all:
+  // const { fill, stroke, ...rest } = schema.props; schema.props = { ...rest, name: iconName }
🧹 Nitpick comments (4)
packages/vue-to-dsl/test/testcases/005_survey/expected/schema.json (1)

199-207: Unify button labeling style; verify Radio mapping

  • TinyButton here uses props.text, while other fixtures use Text children. Pick one convention to avoid UI divergence.
  • Also confirm TinyRadio per-item v-model matches Tiny UI API (vs. a TinyRadioGroup). If group is required, generator should emit group + options.
packages/vue-to-dsl/src/parsers/templateParser.ts (1)

128-144: Handle null literal in parseLiteralExpression

Tiny but useful: treat "null" as a literal to reduce JSExpression fallbacks.

Apply:

   if (trimmed === 'true') return { ok: true, value: true }
   if (trimmed === 'false') return { ok: true, value: false }
+  if (trimmed === 'null') return { ok: true, value: null }
packages/vue-to-dsl/test/testcases/index.test.js (2)

10-11: Filter only directories for test cases

Avoid accidental files matching the pattern.

Apply:

-  const cases = fs.readdirSync(baseDir).filter((name) => /\d+_/.test(name))
+  const cases = fs
+    .readdirSync(baseDir)
+    .filter((name) => /\d+_/.test(name) && fs.statSync(path.join(baseDir, name)).isDirectory())

85-95: Assert no warnings in passing cases (or print for diagnostics)

Catches silent degradations without failing error count.

Apply:

       } else {
         expect(result.errors).toHaveLength(0)
+        expect(result.warnings).toHaveLength(0)
         expect(result.schema).toBeDefined()
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between b5d457e and 7f505f4.

📒 Files selected for processing (6)
  • packages/vue-to-dsl/src/parsers/templateParser.ts (1 hunks)
  • packages/vue-to-dsl/test/testcases/001_simple/expected/schema.json (1 hunks)
  • packages/vue-to-dsl/test/testcases/003_login/expected/schema.json (1 hunks)
  • packages/vue-to-dsl/test/testcases/004_dashboard/expected/schema.json (1 hunks)
  • packages/vue-to-dsl/test/testcases/005_survey/expected/schema.json (1 hunks)
  • packages/vue-to-dsl/test/testcases/index.test.js (1 hunks)
✅ Files skipped from review due to trivial changes (1)
  • packages/vue-to-dsl/test/testcases/001_simple/expected/schema.json
🧰 Additional context used
🧬 Code graph analysis (2)
packages/vue-to-dsl/src/parsers/templateParser.ts (2)
packages/vue-to-dsl/src/index.ts (1)
  • parseTemplate (6-6)
packages/vue-to-dsl/src/parsers/index.ts (1)
  • parseTemplate (1-1)
packages/vue-to-dsl/test/testcases/index.test.js (2)
packages/vue-to-dsl/test/sfc/converter.test.js (1)
  • converter (5-5)
packages/vue-to-dsl/src/converter.ts (1)
  • VueToDslConverter (34-822)
🪛 Biome (2.1.2)
packages/vue-to-dsl/src/parsers/templateParser.ts

[error] 161-161: The regular expression includes this negated empty character class.

Negated empty character classes match anything.
If you want to match against [, escape it [.
Otherwise, remove the character class or fill it.

(lint/correctness/noEmptyCharacterClassInRegex)


[error] 161-161: The regular expression includes this negated empty character class.

Negated empty character classes match anything.
If you want to match against [, escape it [.
Otherwise, remove the character class or fill it.

(lint/correctness/noEmptyCharacterClassInRegex)


[error] 161-161: The regular expression includes this negated empty character class.

Negated empty character classes match anything.
If you want to match against [, escape it [.
Otherwise, remove the character class or fill it.

(lint/correctness/noEmptyCharacterClassInRegex)


[error] 161-161: The regular expression includes this negated empty character class.

Negated empty character classes match anything.
If you want to match against [, escape it [.
Otherwise, remove the character class or fill it.

(lint/correctness/noEmptyCharacterClassInRegex)

🔇 Additional comments (1)
packages/vue-to-dsl/test/testcases/004_dashboard/expected/schema.json (1)

115-407: Fixture looks consistent with conversion rules

Schema structure, bindings, and styles align with parser/generator patterns.

Comment on lines +14 to +21
"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}"
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

Use this.state for method bodies (align with template expressions and other fixtures)

Methods reference state directly; elsewhere bindings use this.state. Standardize to avoid runtime scope mismatches.

Example fix:

- "value": "function onSubmit() {\n  if (!state.form.username || !state.form.password) return\n  console.log('login', state.form)\n}"
+ "value": "function onSubmit() {\n  if (!this.state.form.username || !this.state.form.password) return\n  console.log('login', this.state.form)\n}"

And similarly for onReset.


I'll verify this review comment by examining the repository structure and checking patterns across fixtures and templates.


---

🏁 Script executed:

```shell
#!/bin/bash
# Find the test directory structure
find packages/vue-to-dsl/test/testcases -type d -maxdepth 1 | head -20

Length of output: 317


🏁 Script executed:

#!/bin/bash
# Look for schema.json files with JSFunction entries
find packages/vue-to-dsl/test/testcases -name "schema.json" -type f | head -10

Length of output: 408


🏁 Script executed:

#!/bin/bash
# Search for JSFunction patterns in schema files to see how state is referenced
rg -n "JSFunction" packages/vue-to-dsl/test/testcases -A 3 --type json | head -40

Length of output: 4248


🏁 Script executed:

#!/bin/bash
# Look for template files in test cases
find packages/vue-to-dsl/test/testcases -name "*.vue" -o -name "*.html" | head -10

Length of output: 402


🏁 Script executed:

#!/bin/bash
# Search for 'this.state' vs 'state' patterns in expected schemas
rg -n "this\.state|\"state\." packages/vue-to-dsl/test/testcases --type json

Length of output: 3488



---

🏁 Script executed:

```shell
#!/bin/bash
# Get the exact content of 003_login schema.json to verify the issue
cat packages/vue-to-dsl/test/testcases/003_login/expected/schema.json

Length of output: 9174


🏁 Script executed:

#!/bin/bash
# Get onSearch method from 004_dashboard
rg -n "onSearch" packages/vue-to-dsl/test/testcases/004_dashboard/expected/schema.json -A 3 -B 1

Length of output: 452


Fix method bodies in 003_login to use this.state consistently with template expressions

The fixture has a scope mismatch: methods reference state directly while template bindings use this.state. Template expressions throughout the fixture (and other fixtures like 005_survey) consistently use this.state. Update both onSubmit and onReset methods to use this.state.form instead of bare state.form to align with the template binding pattern.

    "onSubmit": {
      "type": "JSFunction",
-     "value": "function onSubmit() {\n  if (!state.form.username || !state.form.password) return\n  console.log('login', state.form)\n}"
+     "value": "function onSubmit() {\n  if (!this.state.form.username || !this.state.form.password) return\n  console.log('login', this.state.form)\n}"
    },
    "onReset": {
      "type": "JSFunction",
-     "value": "function onReset() {\n  state.form.username = ''\n  state.form.password = ''\n}"
+     "value": "function onReset() {\n  this.state.form.username = ''\n  this.state.form.password = ''\n}"
    }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
"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}"
}
"onSubmit": {
"type": "JSFunction",
"value": "function onSubmit() {\n if (!this.state.form.username || !this.state.form.password) return\n console.log('login', this.state.form)\n}"
},
"onReset": {
"type": "JSFunction",
"value": "function onReset() {\n this.state.form.username = ''\n this.state.form.password = ''\n}"
}
🤖 Prompt for AI Agents
In packages/vue-to-dsl/test/testcases/003_login/expected/schema.json around
lines 14 to 21, the method bodies use the free identifier `state` while template
bindings use `this.state`; update both onSubmit and onReset to reference
`this.state.form` (and `this.state` where applicable) instead of `state.form` so
the methods access the same scoped data as the template expressions, keeping the
rest of the function logic unchanged.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

♻️ Duplicate comments (17)
packages/vue-to-dsl/src/converter.ts (8)

79-85: Pass options to custom script parser for API consistency.

Align with the default parser by passing the same options object including isSetup.

Apply this diff:

           scriptSchema = this.options.customParsers?.script
-            ? this.options.customParsers.script.parse(scriptContent)
+            ? this.options.customParsers.script.parse(scriptContent, {
+                isSetup: !!sfcResult.scriptSetup,
+                ...(this.options as any)
+              })
             : parseScript(scriptContent, {
                 isSetup: !!sfcResult.scriptSetup,
                 ...(this.options as any)
               })

104-107: Pass options to custom style parser for consistency.

Align the custom parser call with the default parser signature.

Apply this diff:

           styleSchema = this.options.customParsers?.style
-            ? this.options.customParsers.style.parse(sfcResult.style)
+            ? this.options.customParsers.style.parse(sfcResult.style, this.options as any)
             : parseStyle(sfcResult.style, this.options as any)

113-119: Avoid mutating instance options; pass a derived object to generator.

Mutating this.options.fileName affects subsequent calls. Create a derived options object instead.

Apply this diff:

-      // 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)
+      const genOptions = {
+        ...(this.options as any),
+        fileName: fileName ? fileName.replace(/\.vue$/i, '') : (this.options as any).fileName
+      }
+      const schema = await generateSchema(templateSchema, scriptSchema, styleSchema, genOptions)

552-591: Ensure temp directory cleanup with finally block.

The temp directory can leak if conversion throws. Wrap in try/finally for guaranteed cleanup.

Apply this diff:

-    // 3) Delegate to convertAppDirectory
-    const schema = await this.convertAppDirectory(appRoot)
-    return schema
+    // 3) Delegate to convertAppDirectory with cleanup
+    let schema: any
+    try {
+      schema = await this.convertAppDirectory(appRoot)
+    } finally {
+      await fs.rm(tmpBase, { recursive: true, force: true }).catch(() => {})
+    }
+    return schema

12-27: Type customParsers to accept options parameter.

The custom parser interface should align with the default parsers which accept options. This allows plugin authors to receive configuration.

Apply this diff:

   customParsers?: {
-    template?: { parse: (code: string) => any }
-    script?: { parse: (code: string) => any }
-    style?: { parse: (code: string) => any }
+    template?: { parse: (code: string, options?: any) => any | any[] }
+    script?: { parse: (code: string, options?: any) => any }
+    style?: { parse: (code: string, options?: any) => any }
   }

67-73: Pass options to custom template parser and normalize return value to array.

Custom parsers should receive options for consistency, and the template parser may return a non-array.

Apply this diff:

           templateSchema = this.options.customParsers?.template
-            ? this.options.customParsers.template.parse(sfcResult.template)
+            ? this.options.customParsers.template.parse(sfcResult.template, this.options)
             : parseTemplate(sfcResult.template, this.options as any)
+          if (!Array.isArray(templateSchema)) {
+            templateSchema = templateSchema ? [templateSchema] : []
+          }

284-289: Critical: Replace Function() with safe parsing to prevent code execution.

Using Function(...)() on user-supplied code from app directories or zip files is a severe security risk. Use a safe parser instead.

Based on learnings


556-574: Zip Slip: Sanitize zip paths to prevent directory traversal.

Malicious zip entries with ../ can write outside the temp directory.

Apply this diff:

-      const outPath = path.join(tmpBase, relPath)
+      const destPath = path.resolve(tmpBase, relPath)
+      if (!destPath.startsWith(tmpBase + path.sep) && destPath !== tmpBase) {
+        return // block path traversal
+      }
       if (file.dir) {
-        writeTasks.push(fs.mkdir(outPath, { recursive: true }))
+        writeTasks.push(fs.mkdir(destPath, { recursive: true }))
       } else {
         fileEntries.push(relPath)
         writeTasks.push(
           (async () => {
-            await fs.mkdir(path.dirname(outPath), { recursive: true })
+            await fs.mkdir(path.dirname(destPath), { recursive: true })
             const content = await file.async('nodebuffer')
-            await fs.writeFile(outPath, content)
+            await fs.writeFile(destPath, content)
           })()
         )
       }
packages/vue-to-dsl/src/index.d.ts (8)

42-50: ScriptSchema should include optional error field.

The parser returns { error } on parse failures, but the type doesn't reflect this.

Apply this diff:

   export interface ScriptSchema {
     state?: Record<string, any>
     methods?: Record<string, any>
     computed?: Record<string, any>
     lifeCycles?: Record<string, any>
     imports?: ImportInfo[]
     props?: PropInfo[]
     emits?: string[]
+    error?: string
   }

141-154: Add options parameter to parser function signatures.

All parser implementations accept an optional options parameter that's missing from the declarations.

Apply this diff:

-  export function parseTemplate(template: string): TemplateSchema[]
+  export function parseTemplate(template: string, options?: any): TemplateSchema[]

-  export function parseScript(script: string): ScriptSchema
+  export function parseScript(script: string, options?: any): ScriptSchema

-  export function parseStyle(style: string): StyleSchema
+  export function parseStyle(style: string, options?: any): StyleSchema

141-155: Add missing generateAppSchema export to public API.

The function is exported from index.ts but lacks type declarations here.

Add after line 154:

   export function parseStyle(style: string): StyleSchema
+
+  /**
+   * 生成应用级 DSL Schema(App)
+   */
+  export function generateAppSchema(pageSchemas: PageSchema[], options?: any): any
 }

2-17: VueToSchemaOptions is missing fields and parser signatures are incomplete.

The interface lacks fileName, path, title, and description used by the implementation, and parser types don't accept options.

Apply this diff:

   export interface VueToSchemaOptions {
     // 组件映射配置
     componentMap?: Record<string, string>
     // 是否保留注释
     preserveComments?: boolean
     // 是否严格模式
     strictMode?: boolean
+    // 控制是否输出 computed 字段(默认 false)
+    computed_flag?: boolean
     // 自定义解析器
     customParsers?: {
       template?: TemplateParser
       script?: ScriptParser
       style?: StyleParser
     }
+    // 生成参数
+    fileName?: string
+    path?: string
+    title?: string
+    description?: string
   }

19-21: TemplateParser should return an array to match implementation.

The parser returns TemplateSchema[] but the type declares a single object.

Apply this diff:

   export interface TemplateParser {
-    parse(template: string, options?: any): TemplateSchema
+    parse(template: string, options?: any): TemplateSchema[]
   }

71-83: Make PageSchema.path optional to match generator output.

The generator doesn't guarantee a path field, so requiring it causes type errors.

Apply this diff:

   export interface PageSchema {
     componentName: 'Page'
     fileName: string
-    path: string
+    path?: string
     meta?: Record<string, any>

85-90: ConvertResult.schema can be null on conversion failure.

The implementation returns { schema: null } on errors, but the type doesn't allow null.

Apply this diff:

   export interface ConvertResult {
-    schema: PageSchema
+    schema: PageSchema | null
     dependencies: string[]
     errors: string[]
     warnings: string[]
   }

134-139: Fix generateSchema signature to accept parsed schemas.

The implementation accepts TemplateSchema[], ScriptSchema, and StyleSchema, not strings.

Apply this diff:

   export function generateSchema(
-    template: string,
-    script: string,
-    style?: string,
+    template: TemplateSchema[],
+    script: ScriptSchema,
+    style?: StyleSchema,
     options?: VueToSchemaOptions
   ): Promise<PageSchema>
packages/vue-to-dsl/test/full/index.test.js (1)

6-10: Define __dirname in ESM/Vitest to avoid ReferenceError.

Using __dirname with import syntax will fail under ESM. Define it from import.meta.url before use.

Apply this diff:

 import { describe, it, expect } from 'vitest'
 import { VueToDslConverter } from '../../src/converter'
 import fs from 'fs'
 import path from 'path'
+import { fileURLToPath } from 'node:url'

+const __filename = fileURLToPath(import.meta.url)
+const __dirname = path.dirname(__filename)
 const fullDir = __dirname
 const inputDir = path.join(fullDir, 'input')
🧹 Nitpick comments (1)
packages/vue-to-dsl/src/converter.ts (1)

606-640: Consider validating gitignore patterns to prevent ReDoS.

Building regex from gitignore content could be exploited with malicious patterns. Consider adding pattern validation or using a tested gitignore library.

For production use, consider using a library like ignore (npm) which handles gitignore patterns safely, or add regex complexity validation before constructing patterns.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 7f505f4 and 966a400.

📒 Files selected for processing (8)
  • packages/vue-to-dsl/README.md (1 hunks)
  • packages/vue-to-dsl/cli.ts (1 hunks)
  • packages/vue-to-dsl/src/converter.ts (1 hunks)
  • packages/vue-to-dsl/src/generator/index.ts (1 hunks)
  • packages/vue-to-dsl/src/index.d.ts (1 hunks)
  • packages/vue-to-dsl/test/full/index.test.js (1 hunks)
  • packages/vue-to-dsl/test/sfc/converter.test.js (1 hunks)
  • packages/vue-to-dsl/test/testcases/index.test.js (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (4)
  • packages/vue-to-dsl/test/testcases/index.test.js
  • packages/vue-to-dsl/src/generator/index.ts
  • packages/vue-to-dsl/cli.ts
  • packages/vue-to-dsl/test/sfc/converter.test.js
🧰 Additional context used
🧠 Learnings (4)
📚 Learning: 2025-01-14T06:50:21.158Z
Learnt from: rhlin
PR: opentiny/tiny-engine#1011
File: packages/canvas/render/src/data-function/parser.ts:191-195
Timestamp: 2025-01-14T06:50:21.158Z
Learning: The `newFn` function in `packages/canvas/render/src/data-function/parser.ts` has a known security issue with evaluating untrusted code through `data.value`. This was identified during code review but intentionally deferred as the original implementation was kept during code refactoring.

Applied to files:

  • packages/vue-to-dsl/src/converter.ts
📚 Learning: 2025-01-14T06:58:38.661Z
Learnt from: rhlin
PR: opentiny/tiny-engine#1011
File: packages/canvas/render/src/data-utils.ts:8-12
Timestamp: 2025-01-14T06:58:38.661Z
Learning: The use of Function constructor in `packages/canvas/render/src/data-utils.ts` is part of migrated code and was intentionally kept as-is during migration, despite potential security concerns.

Applied to files:

  • packages/vue-to-dsl/src/converter.ts
📚 Learning: 2025-07-03T09:22:59.512Z
Learnt from: hexqi
PR: opentiny/tiny-engine#1501
File: mockServer/src/tool/Common.js:79-82
Timestamp: 2025-07-03T09:22:59.512Z
Learning: In the tiny-engine project, the mockServer code uses ES6 import syntax but is compiled to CommonJS output. This means CommonJS globals like `__dirname` are available at runtime, while ES6 module-specific features like `import.meta` would cause runtime errors.

Applied to files:

  • packages/vue-to-dsl/test/full/index.test.js
📚 Learning: 2024-09-30T07:51:10.036Z
Learnt from: chilingling
PR: opentiny/tiny-engine#837
File: packages/vue-generator/src/plugins/genDependenciesPlugin.js:66-66
Timestamp: 2024-09-30T07:51:10.036Z
Learning: In the `tiny-engine` project, `opentiny/tiny-engine-dsl-vue` refers to the current package itself, and importing types from it may cause circular dependencies.

Applied to files:

  • packages/vue-to-dsl/README.md
🧬 Code graph analysis (3)
packages/vue-to-dsl/src/converter.ts (6)
packages/vue-to-dsl/src/index.d.ts (3)
  • VueToSchemaOptions (2-17)
  • ConvertResult (85-90)
  • VueToDslConverter (92-109)
packages/vue-to-dsl/src/constants.ts (1)
  • defaultComponentMap (1-16)
packages/vue-to-dsl/src/parser/index.ts (1)
  • parseSFC (4-41)
packages/vue-to-dsl/src/parsers/scriptParser.ts (1)
  • parseScript (460-488)
packages/vue-to-dsl/src/parsers/styleParser.ts (1)
  • parseStyle (1-6)
packages/vue-to-dsl/src/generator/index.ts (2)
  • generateSchema (133-158)
  • generateAppSchema (160-173)
packages/vue-to-dsl/src/index.d.ts (3)
packages/vue-to-dsl/src/converter.ts (3)
  • VueToSchemaOptions (12-27)
  • ConvertResult (29-34)
  • VueToDslConverter (36-825)
packages/vue-to-dsl/src/generator/index.ts (1)
  • generateSchema (133-158)
packages/vue-to-dsl/src/parsers/scriptParser.ts (1)
  • parseScript (460-488)
packages/vue-to-dsl/test/full/index.test.js (2)
packages/vue-to-dsl/test/sfc/converter.test.js (1)
  • converter (5-5)
packages/vue-to-dsl/test/testcases/index.test.js (1)
  • converter (8-8)
🪛 ast-grep (0.39.6)
packages/vue-to-dsl/src/converter.ts

[warning] 624-624: Regular expression constructed from variable input detected. This can lead to Regular Expression Denial of Service (ReDoS) attacks if the variable contains malicious patterns. Use libraries like 'recheck' to validate regex safety or use static patterns.
Context: new RegExp(^${regexString})
Note: [CWE-1333] Inefficient Regular Expression Complexity [REFERENCES]
- https://owasp.org/www-community/attacks/Regular_expression_Denial_of_Service_-_ReDoS
- https://cwe.mitre.org/data/definitions/1333.html

(regexp-from-variable)


[warning] 627-627: Regular expression constructed from variable input detected. This can lead to Regular Expression Denial of Service (ReDoS) attacks if the variable contains malicious patterns. Use libraries like 'recheck' to validate regex safety or use static patterns.
Context: new RegExp(^${regexString}(/.*)?$)
Note: [CWE-1333] Inefficient Regular Expression Complexity [REFERENCES]
- https://owasp.org/www-community/attacks/Regular_expression_Denial_of_Service_-_ReDoS
- https://cwe.mitre.org/data/definitions/1333.html

(regexp-from-variable)

🪛 LanguageTool
packages/vue-to-dsl/README.md

[uncategorized] ~172-~172: 您的意思是“"不"齐”?
Context: ...定(页面级) - 根节点 componentName: 'Page',自动补齐 id(8 位字母数字) - state/methods/`com...

(BU)

@hexqi hexqi merged commit ba37154 into opentiny:ospp-2025/source-to-dsl Oct 29, 2025
1 check passed
xuanlid added a commit to xuanlid/tiny-engine that referenced this pull request Mar 10, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request ospp ospp

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants