Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 15 additions & 7 deletions packages/vue-generator/src/generator/vue/sfc/generateAttribute.js
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,10 @@ const handleJSExpressionBinding = (key, value, isJSX) => {
}

// expression 使用 v-bind 绑定
return `:${key}="${expressValue}"`
// 如果包含双引号,通过 " 编码避免与属性分隔符冲突
// 比如绑定的值为:[{ "name": "test" }]
// 则转换为: :key="[{ "name": "test" }]"
return `:${key}="${expressValue.replaceAll(/"/g, '"')}"`
}

const handleBindI18n = (key, value, isJSX) => {
Expand Down Expand Up @@ -176,13 +179,13 @@ export const handleLoopAttrHook = (schemaData = {}, globalHooks, config) => {
if (loop?.value && loop?.type) {
source = loop.value.replace(isJSX ? thisRegexp : thisPropsBindRe, '')
} else {
source = JSON.stringify(loop).replaceAll("'", "\\'").replaceAll(/"/g, "'")
source = JSON.stringify(loop)
}

const iterVar = [...loopArgs]

if (!isJSX) {
attributes.push(`v-for="(${iterVar.join(',')}) in ${source}"`)
attributes.push(`v-for="(${iterVar.join(',')}) in ${source.replaceAll(/"/g, '"')}"`)

return
}
Expand Down Expand Up @@ -455,7 +458,11 @@ const transformObjValue = (renderKey, value, globalHooks, config, transformObjTy
const result = { shouldBindToState: false, res: null }

if (typeof value === 'string') {
result.res = `${renderKey}"${value.replaceAll("'", "\\'").replaceAll(/"/g, "'")}"`
result.res = `${renderKey}"${value
.replaceAll(/\\/g, '\\\\')
.replaceAll(/\n/g, '\\n')
.replaceAll(/\r/g, '\\r')
.replaceAll(/"/g, '\\"')}"`

return result
}
Expand Down Expand Up @@ -575,15 +582,16 @@ export const handleObjBindAttrHook = (schemaData, globalHooks, config) => {
if (shouldBindToState && !isJSX) {
let stateKey = key
let addSuccess = globalHooks.addState(stateKey, `${stateKey}:${res}`)
let retryCount = 0

while (!addSuccess) {
while (!addSuccess && retryCount++ < 100) {
stateKey = `${key}${randomString()}`
addSuccess = globalHooks.addState(stateKey, `${stateKey}:${res}`)
}

attributes.push(`:${key}="state.${stateKey}"`)
Comment on lines 582 to 592
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

Don't emit state.${stateKey} when state registration never succeeds.

The retry cap removes the infinite loop, but if addState is still false after 100 attempts, Line 592 still references a state key that was never registered. That swaps a hang for silently broken generated code.

Possible fix
       while (!addSuccess && retryCount++ < 100) {
         stateKey = `${key}${randomString()}`
         addSuccess = globalHooks.addState(stateKey, `${stateKey}:${res}`)
       }

+      if (!addSuccess) {
+        throw new Error(`Failed to register generated state for prop "${key}" after 100 attempts`)
+      }
+
       attributes.push(`:${key}="state.${stateKey}"`)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/vue-generator/src/generator/vue/sfc/generateAttribute.js` around
lines 582 - 592, The code currently always pushes
attributes.push(`:${key}="state.${stateKey}"`) even if globalHooks.addState
never succeeded; update the block in the shouldBindToState && !isJSX branch
(variables: stateKey, addSuccess, retryCount, globalHooks.addState) to check
addSuccess after the retry loop and only emit the bound attribute when
addSuccess is true; if addSuccess is false after 100 attempts, emit a safe
fallback (e.g., the original non-bound attribute or a literal value derived from
res) or skip adding the binding entirely so the generated code does not
reference an unregistered state key. Ensure any fallback preserves original
behavior when binding fails.

} else {
attributes.push(isJSX ? `${key}={${res}}` : `:${key}="${res.replaceAll(/"/g, "'")}"`)
attributes.push(isJSX ? `${key}={${res}}` : `:${key}="${res.replaceAll(/"/g, '&quot;')}"`)
}

delete props[key]
Expand All @@ -600,7 +608,7 @@ export const handlePrimitiveAttributeHook = (schemaData, globalHooks, config) =>
const valueType = typeof value

if (valueType === 'string') {
attributes.push(`${key}="${value.replaceAll(/"/g, "'")}"`)
attributes.push(`${key}="${value.replaceAll(/"/g, '&quot;')}"`)

delete props[key]
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ const state = vue.reactive({
nullValue: null,
numberValue: 0,
emptyStr: '',
strVal: 'i am str.',
strVal: "i am 'str'.",
trueVal: true,
falseVal: false,
arrVal: [1, '2', { aaa: 'aaa' }, [3, 4], true, false],
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@
}
},
"strVal": {
"defaultValue": "i am str.",
"defaultValue": "i am 'str'.",
"accessor": {
"getter": {
"type": "JSFunction",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -118,7 +118,7 @@ const { utils } = wrap(function () {
})()
const state = vue.reactive({
IconPlusSquare: utils.IconPlusSquare(),
theme: "{ 'id': 22, 'name': '@cloud/tinybuilder-theme-dark', 'description': '黑暗主题' }",
theme: '{ "id": 22, "name": "@cloud/tinybuilder-theme-dark", "description": "黑暗主题" }',
companyName: '',
companyOptions: null,
companyCity: '',
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
[
{
"componentName": "TinyButton",
"exportName": "Button",
"package": "@opentiny/vue",
"version": "^3.10.0",
"destructuring": true
}
]
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
<template>
<div>
<tiny-grid :columns="state.columns"></tiny-grid>
</div>
</template>

<script setup lang="jsx">
import { Grid as TinyGrid } from '@opentiny/vue'
import * as vue from 'vue'
import { defineProps, defineEmits } from 'vue'
import { I18nInjectionKey } from 'vue-i18n'

const props = defineProps({})

const emit = defineEmits([])
const { t, lowcodeWrap, stores } = vue.inject(I18nInjectionKey).lowcode()
const wrap = lowcodeWrap(props, { emit })
wrap({ stores })

const state = vue.reactive({
columns: [
{ field: 'info', title: '信息', slots: { default: ({ row }, h) => <span title='{"key": "value"}'></span> } }
]
})
wrap({ state })
</script>
<style scoped></style>
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
<template>
<div>
<tiny-button
multilineJson='{
"name": "test",
"value": "data"
}'
:objWithMultiline="{ template: 'line1\nline2', label: 'normal' }"
></tiny-button>
</div>
</template>

<script setup>
import * as vue from 'vue'
import { defineProps, defineEmits } from 'vue'
import { I18nInjectionKey } from 'vue-i18n'

const props = defineProps({})

const emit = defineEmits([])
const { t, lowcodeWrap, stores } = vue.inject(I18nInjectionKey).lowcode()
const wrap = lowcodeWrap(props, { emit })
wrap({ stores })

const state = vue.reactive({})
wrap({ state })
</script>
<style scoped></style>
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
<template>
<div>
<tiny-button
noQuotes="plain text value"
onlyDouble='{"name": "test", "id": "main"}'
onlySingle="it's a 'test' value"
bothQuotes='She said "hello" and it&apos;s fine'
htmlAttr='class="primary" id="btn-1"'
emptyStr=""
></tiny-button>
</div>
</template>

<script setup>
import * as vue from 'vue'
import { defineProps, defineEmits } from 'vue'
import { I18nInjectionKey } from 'vue-i18n'

const props = defineProps({})

const emit = defineEmits([])
const { t, lowcodeWrap, stores } = vue.inject(I18nInjectionKey).lowcode()
const wrap = lowcodeWrap(props, { emit })
wrap({ stores })

const state = vue.reactive({})
wrap({ state })
</script>
<style scoped></style>
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
<template>
<div>
<tiny-button
jsonContent='{"key": "value", "nested": "data"}'
mixedQuotes="She said &quot;hello&quot; and he said 'hi'"
:filteredItems="state.items.filter((i) => i.name === 'test' && i.tag === 'featured')"
></tiny-button>
</div>
</template>

<script setup>
import * as vue from 'vue'
import { defineProps, defineEmits } from 'vue'
import { I18nInjectionKey } from 'vue-i18n'

const props = defineProps({})

const emit = defineEmits([])
const { t, lowcodeWrap, stores } = vue.inject(I18nInjectionKey).lowcode()
const wrap = lowcodeWrap(props, { emit })
wrap({ stores })

const state = vue.reactive({
items: [
{ name: 'test', tag: 'featured' },
{ name: 'hello', tag: 'normal' }
]
})
wrap({ state })
</script>
<style scoped></style>
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
<template>
<div>
<tiny-button
v-for="(item, index) in [
{
type: 'primary',
subStr: 'primary\'subStr\''
},
{
type: ''
},
{
type: 'info'
},
{
type: 'success'
},
{
type: 'warning'
},
{
type: 'danger'
}
]"
type="primary"
text="test"
subStr="pri&quot;ma&quot;ry'subStr'"
:customExpressionTest="{
value: [
{
defaultValue: '{&quot;class&quot;: &quot;test-class&quot;, &quot;id&quot;: &quot;test-id&quot;}'
}
]
}"
:customAttrTest="{
value: [
{
defaultValue:
'{&quot;class&quot;: &quot;test-class&quot;, &quot;id&quot;: &quot;test-id&quot;, &quot;class2&quot;: &quot;te\'st\'-class2&quot;}',
subStr: 'test-\'cl\'ass2'
}
]
}"
></tiny-button>
</div>
</template>

<script setup>
import * as vue from 'vue'
import { defineProps, defineEmits } from 'vue'
import { I18nInjectionKey } from 'vue-i18n'

const props = defineProps({})

const emit = defineEmits([])
const { t, lowcodeWrap, stores } = vue.inject(I18nInjectionKey).lowcode()
const wrap = lowcodeWrap(props, { emit })
wrap({ stores })

const state = vue.reactive({
customAttrTest: { value: [{ defaultValue: '{"class": "test-class", "id": "test-id"}' }] }
})
wrap({ state })
</script>
<style scoped></style>
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
[
{
"componentName": "TinyGrid",
"exportName": "Grid",
"package": "@opentiny/vue",
"version": "^3.10.0",
"destructuring": true
}
]
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
{
"state": {
"columns": [
{
"field": "info",
"title": "信息",
"slots": {
"default": {
"type": "JSSlot",
"value": [
{
"componentName": "span",
"props": {
"title": "{\"key\": \"value\"}"
},
"id": "jsx-span-001",
"children": []
}
],
"params": ["row"]
}
}
}
]
},
"methods": {},
"componentName": "Page",
"css": "",
"props": {},
"lifeCycles": {},
"children": [
{
"componentName": "TinyGrid",
"props": {
"columns": {
"type": "JSExpression",
"value": "state.columns"
}
},
"id": "jsx-test-001",
"children": []
}
],
"fileName": "testJsxQuote"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
{
"state": {},
"methods": {},
"componentName": "Page",
"css": "",
"props": {},
"lifeCycles": {},
"children": [
{
"componentName": "TinyButton",
"props": {
"multilineJson": "{\n \"name\": \"test\",\n \"value\": \"data\"\n}",
"objWithMultiline": {
"template": "line1\nline2",
"label": "normal"
}
},
"id": "multiline-test-001",
"children": []
}
],
"fileName": "testMultiline"
}
Loading
Loading