From 344c03702366766e6205f90073677264c3301b96 Mon Sep 17 00:00:00 2001 From: chilingling Date: Thu, 5 Jun 2025 11:09:07 +0800 Subject: [PATCH 1/2] fix: generate code should convert quote on vue template --- .../generator/vue/sfc/generateAttribute.js | 59 +++++++++++++---- .../sfc/accessor/expected/Accessor.vue | 2 +- .../test/testcases/sfc/accessor/schema.json | 2 +- .../sfc/templateQuote/components-map.json | 9 +++ .../templateQuote/expected/templateQuote.vue | 66 +++++++++++++++++++ .../sfc/templateQuote/page.schema.json | 46 +++++++++++++ .../sfc/templateQuote/templateQuote.test.js | 33 ++++++++++ 7 files changed, 202 insertions(+), 15 deletions(-) create mode 100644 packages/vue-generator/test/testcases/sfc/templateQuote/components-map.json create mode 100644 packages/vue-generator/test/testcases/sfc/templateQuote/expected/templateQuote.vue create mode 100644 packages/vue-generator/test/testcases/sfc/templateQuote/page.schema.json create mode 100644 packages/vue-generator/test/testcases/sfc/templateQuote/templateQuote.test.js diff --git a/packages/vue-generator/src/generator/vue/sfc/generateAttribute.js b/packages/vue-generator/src/generator/vue/sfc/generateAttribute.js index 2f4c8e898e..066cb9649d 100644 --- a/packages/vue-generator/src/generator/vue/sfc/generateAttribute.js +++ b/packages/vue-generator/src/generator/vue/sfc/generateAttribute.js @@ -70,7 +70,7 @@ export const checkHasSpecialType = (obj) => { return false } -const handleJSExpressionBinding = (key, value, isJSX) => { +const handleJSExpressionBinding = (key, value, isJSX, globalHooks) => { const expressValue = value.value.replace(isJSX ? thisRegexp : thisPropsBindRe, '') if (isJSX) { @@ -85,7 +85,19 @@ const handleJSExpressionBinding = (key, value, isJSX) => { } // expression 使用 v-bind 绑定 - return `:${key}="${expressValue}"` + if (expressValue.includes('"')) { + let stateKey = `${key}_${randomString()}` + let addSuccess = globalHooks.addState(stateKey, `${stateKey}:${expressValue}`) + + while (!addSuccess) { + stateKey = `${key}_${randomString()}` + addSuccess = globalHooks.addState(stateKey, `${stateKey}:${expressValue}`) + } + + return `:${key}="state.${stateKey}"` + } else { + return `:${key}="${expressValue}"` + } } const handleBindI18n = (key, value, isJSX) => { @@ -182,7 +194,19 @@ export const handleLoopAttrHook = (schemaData = {}, globalHooks, config) => { const iterVar = [...loopArgs] if (!isJSX) { - attributes.push(`v-for="(${iterVar.join(',')}) in ${source}"`) + if (source.includes('"')) { + let stateKey = `loop_${randomString()}` + let addSuccess = globalHooks.addState(stateKey, `${stateKey}:${source}`) + + while (!addSuccess) { + stateKey = `loop_${randomString()}` + addSuccess = globalHooks.addState(stateKey, `${stateKey}:${source}`) + } + + attributes.push(`v-for="(${iterVar.join(',')}) in state.${stateKey}"`) + } else { + attributes.push(`v-for="(${iterVar.join(',')}) in ${source}"`) + } return } @@ -352,7 +376,7 @@ export const handleExpressionAttrHook = (schemaData, globalHooks, config) => { Object.entries(props).forEach(([key, value]) => { if (value?.type === JS_EXPRESSION && !isOn(key)) { specialTypeHandler[JS_RESOURCE](value, globalHooks, config) - attributes.push(handleJSExpressionBinding(key, value, isJSX)) + attributes.push(handleJSExpressionBinding(key, value, isJSX, globalHooks)) delete props[key] } @@ -384,7 +408,7 @@ export const handleJSFunctionAttrHook = (schemaData, globalHooks, config) => { functionName = value.value } - attributes.push(handleJSExpressionBinding(key, { value: functionName }, isJSX)) + attributes.push(handleJSExpressionBinding(key, { value: functionName }, isJSX, globalHooks)) delete props[key] } @@ -451,11 +475,15 @@ const genStateAccessor = (value, globalHooks) => { } } -const transformObjValue = (renderKey, value, globalHooks, config, transformObjType) => { +const transformObjValue = (renderKey, value, globalHooks, config, transformObjType, shouldConvertQuote = false) => { const result = { shouldBindToState: false, res: null } if (typeof value === 'string') { - result.res = `${renderKey}"${value.replaceAll("'", "\\'").replaceAll(/"/g, "'")}"` + if (shouldConvertQuote) { + result.res = `${renderKey}'${value.replaceAll(/"/g, "'").replaceAll(/'/g, "\\'")}'` + } else { + result.res = `${renderKey}"${value.replaceAll("'", "\\'").replaceAll(/"/g, "'")}"` + } return result } @@ -468,7 +496,11 @@ const transformObjValue = (renderKey, value, globalHooks, config, transformObjTy if (specialTypeHandler[value?.type]) { const specialVal = specialTypeHandler[value.type](value, globalHooks, config)?.value || '' - result.res = `${renderKey}${specialVal}` + if (shouldConvertQuote) { + result.res = `${renderKey}${specialVal.replaceAll(/"/g, "'")}` + } else { + result.res = `${renderKey}${specialVal}` + } if (specialTypes.includes(value.type)) { result.shouldBindToState = true @@ -503,7 +535,7 @@ const transformObjValue = (renderKey, value, globalHooks, config, transformObjTy } const normalKeyRegexp = /^[a-zA-Z_$][a-zA-Z0-9_$]*$/ -export const transformObjType = (obj, globalHooks, config) => { +export const transformObjType = (obj, globalHooks, config, shouldConvertQuote = false) => { if (!obj || typeof obj !== 'object') { return { res: obj @@ -527,7 +559,8 @@ export const transformObjType = (obj, globalHooks, config) => { value, globalHooks, config, - transformObjType + transformObjType, + shouldConvertQuote ) if (tmpShouldBindToState) { @@ -541,7 +574,7 @@ export const transformObjType = (obj, globalHooks, config) => { // 复杂的 object 类型,需要递归处理 const { res: tempRes, shouldBindToState: tempShouldBindToState } = - transformObjType(value, globalHooks, config) || {} + transformObjType(value, globalHooks, config, shouldConvertQuote) || {} resStr.push(`${renderKey}${tempRes}`) @@ -570,7 +603,7 @@ export const handleObjBindAttrHook = (schemaData, globalHooks, config) => { return } - const { res, shouldBindToState } = transformObjType(value, globalHooks, config) + const { res, shouldBindToState } = transformObjType(value, globalHooks, config, true) if (shouldBindToState && !isJSX) { let stateKey = key @@ -583,7 +616,7 @@ export const handleObjBindAttrHook = (schemaData, globalHooks, config) => { attributes.push(`:${key}="state.${stateKey}"`) } else { - attributes.push(isJSX ? `${key}={${res}}` : `:${key}="${res.replaceAll(/"/g, "'")}"`) + attributes.push(isJSX ? `${key}={${res}}` : `:${key}="${res}"`) } delete props[key] diff --git a/packages/vue-generator/test/testcases/sfc/accessor/expected/Accessor.vue b/packages/vue-generator/test/testcases/sfc/accessor/expected/Accessor.vue index c515a9711d..b524540906 100644 --- a/packages/vue-generator/test/testcases/sfc/accessor/expected/Accessor.vue +++ b/packages/vue-generator/test/testcases/sfc/accessor/expected/Accessor.vue @@ -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], diff --git a/packages/vue-generator/test/testcases/sfc/accessor/schema.json b/packages/vue-generator/test/testcases/sfc/accessor/schema.json index 204121eec1..0935a984a8 100644 --- a/packages/vue-generator/test/testcases/sfc/accessor/schema.json +++ b/packages/vue-generator/test/testcases/sfc/accessor/schema.json @@ -44,7 +44,7 @@ } }, "strVal": { - "defaultValue": "i am str.", + "defaultValue": "i am 'str'.", "accessor": { "getter": { "type": "JSFunction", diff --git a/packages/vue-generator/test/testcases/sfc/templateQuote/components-map.json b/packages/vue-generator/test/testcases/sfc/templateQuote/components-map.json new file mode 100644 index 0000000000..b84fe3b474 --- /dev/null +++ b/packages/vue-generator/test/testcases/sfc/templateQuote/components-map.json @@ -0,0 +1,9 @@ +[ + { + "componentName": "TinyButton", + "exportName": "Button", + "package": "@opentiny/vue", + "version": "^3.10.0", + "destructuring": true + } +] diff --git a/packages/vue-generator/test/testcases/sfc/templateQuote/expected/templateQuote.vue b/packages/vue-generator/test/testcases/sfc/templateQuote/expected/templateQuote.vue new file mode 100644 index 0000000000..a660e68df5 --- /dev/null +++ b/packages/vue-generator/test/testcases/sfc/templateQuote/expected/templateQuote.vue @@ -0,0 +1,66 @@ + + + + diff --git a/packages/vue-generator/test/testcases/sfc/templateQuote/page.schema.json b/packages/vue-generator/test/testcases/sfc/templateQuote/page.schema.json new file mode 100644 index 0000000000..534af857a8 --- /dev/null +++ b/packages/vue-generator/test/testcases/sfc/templateQuote/page.schema.json @@ -0,0 +1,46 @@ +{ + "state": { + "customAttrTest": { + "value": [ + { + "defaultValue": "{\"class\": \"test-class\", \"id\": \"test-id\"}" + } + ] + } + }, + "methods": {}, + "componentName": "Page", + "css": "", + "props": {}, + "lifeCycles": {}, + "children": [ + { + "componentName": "TinyButton", + "props": { + "type": "primary", + "text": "test", + "subStr": "pri\"ma\"ry'subStr'", + "customAttrTest": { + "value": [ + { + "defaultValue": "{\"class\": \"test-class\", \"id\": \"test-id\", \"class2\": \"te'st'-class2\"}", + "subStr": "test-'cl'ass2" + } + ] + }, + "customExpressionTest": { + "type": "JSExpression", + "value": "{\n \"value\": [\n {\n \"defaultValue\": \"{\\\"class\\\": \\\"test-class\\\", \\\"id\\\": \\\"test-id\\\"}\"\n }\n ]\n}" + } + }, + "loopArgs": ["item", "index"], + "loop": { + "type": "JSExpression", + "value": "[\n {\n type: 'primary'\n, subStr: \"primary'subStr'\"\n },\n {\n type: \"\"\n },\n {\n type: \"info\"\n },\n {\n type: \"success\"\n },\n {\n type: \"warning\"\n },\n {\n type: \"danger\"\n }\n]" + }, + "id": "63623253", + "children": [] + } + ], + "fileName": "testTemplateQuote" +} diff --git a/packages/vue-generator/test/testcases/sfc/templateQuote/templateQuote.test.js b/packages/vue-generator/test/testcases/sfc/templateQuote/templateQuote.test.js new file mode 100644 index 0000000000..4f6d0672e4 --- /dev/null +++ b/packages/vue-generator/test/testcases/sfc/templateQuote/templateQuote.test.js @@ -0,0 +1,33 @@ +import { expect, test, beforeEach, afterEach, vi } from 'vitest' +import { genSFCWithDefaultPlugin } from '@/generator/vue/sfc' +import pageSchema from './page.schema.json' +import { formatCode } from '@/utils/formatCode' + +let count = 0 +const mockValue = [0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9] + +beforeEach(() => { + // 伪随机数,保证每次快照都一致 + vi.spyOn(global.Math, 'random').mockImplementation(() => { + const res = mockValue[count] + + count++ + if (count > 10) { + count = 0 + } + + return res + }) +}) + +afterEach(() => { + vi.spyOn(global.Math, 'random').mockRestore() +}) + +test('should generate template quote correctly', async () => { + const res = genSFCWithDefaultPlugin(pageSchema, []) + + const formattedCode = formatCode(res, 'vue') + + await expect(formattedCode).toMatchFileSnapshot('./expected/templateQuote.vue') +}) From e96d6732a21a607b55797f8e2b065111e14ed377 Mon Sep 17 00:00:00 2001 From: chilingling Date: Mon, 16 Jun 2025 14:23:27 +0800 Subject: [PATCH 2/2] fix: only save to state when contain both double and single quote --- .../src/generator/vue/sfc/generateAttribute.js | 10 +++++----- .../sfc/templateQuote/expected/templateQuote.vue | 15 +++++++-------- 2 files changed, 12 insertions(+), 13 deletions(-) diff --git a/packages/vue-generator/src/generator/vue/sfc/generateAttribute.js b/packages/vue-generator/src/generator/vue/sfc/generateAttribute.js index 066cb9649d..2e147e3d33 100644 --- a/packages/vue-generator/src/generator/vue/sfc/generateAttribute.js +++ b/packages/vue-generator/src/generator/vue/sfc/generateAttribute.js @@ -85,7 +85,7 @@ const handleJSExpressionBinding = (key, value, isJSX, globalHooks) => { } // expression 使用 v-bind 绑定 - if (expressValue.includes('"')) { + if (expressValue.includes('"') && expressValue.includes("'")) { let stateKey = `${key}_${randomString()}` let addSuccess = globalHooks.addState(stateKey, `${stateKey}:${expressValue}`) @@ -96,7 +96,7 @@ const handleJSExpressionBinding = (key, value, isJSX, globalHooks) => { return `:${key}="state.${stateKey}"` } else { - return `:${key}="${expressValue}"` + return `:${key}="${expressValue.replaceAll(/"/g, "'")}"` } } @@ -188,13 +188,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) { - if (source.includes('"')) { + if (source.includes('"') && source.includes("'")) { let stateKey = `loop_${randomString()}` let addSuccess = globalHooks.addState(stateKey, `${stateKey}:${source}`) @@ -205,7 +205,7 @@ export const handleLoopAttrHook = (schemaData = {}, globalHooks, config) => { attributes.push(`v-for="(${iterVar.join(',')}) in state.${stateKey}"`) } else { - attributes.push(`v-for="(${iterVar.join(',')}) in ${source}"`) + attributes.push(`v-for="(${iterVar.join(',')}) in ${source.replaceAll(/"/g, "'")}"`) } return diff --git a/packages/vue-generator/test/testcases/sfc/templateQuote/expected/templateQuote.vue b/packages/vue-generator/test/testcases/sfc/templateQuote/expected/templateQuote.vue index a660e68df5..27197c4583 100644 --- a/packages/vue-generator/test/testcases/sfc/templateQuote/expected/templateQuote.vue +++ b/packages/vue-generator/test/testcases/sfc/templateQuote/expected/templateQuote.vue @@ -5,7 +5,13 @@ type="primary" text="test" subStr="pri'ma'ry'subStr'" - :customExpressionTest="state.customExpressionTest_vBHN" + :customExpressionTest="{ + value: [ + { + defaultValue: '{\'class\': \'test-class\', \'id\': \'test-id\'}' + } + ] + }" :customAttrTest="{ value: [ { @@ -52,13 +58,6 @@ const state = vue.reactive({ type: 'danger' } ], - customExpressionTest_vBHN: { - value: [ - { - defaultValue: '{"class": "test-class", "id": "test-id"}' - } - ] - }, customAttrTest: { value: [{ defaultValue: "{'class': 'test-class', 'id': 'test-id'}" }] } }) wrap({ state })