Skip to content

Commit 690ef29

Browse files
authored
fix(compiler-sfc): handle prop keys that need escaping (#7803)
close #8291
1 parent aa1e77d commit 690ef29

File tree

4 files changed

+111
-6
lines changed

4 files changed

+111
-6
lines changed

packages/compiler-sfc/__tests__/compileScript/__snapshots__/definePropsDestructure.spec.ts.snap

+38
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,44 @@ return () => {}
9797
}"
9898
`;
9999

100+
exports[`sfc reactive props destructure > default values w/ runtime declaration & key is string 1`] = `
101+
"import { mergeDefaults as _mergeDefaults } from 'vue'
102+
103+
export default {
104+
props: _mergeDefaults(['foo', 'foo:bar'], {
105+
foo: 1,
106+
\\"foo:bar\\": 'foo-bar'
107+
}),
108+
setup(__props) {
109+
110+
111+
112+
return () => {}
113+
}
114+
115+
}"
116+
`;
117+
118+
exports[`sfc reactive props destructure > default values w/ type declaration & key is string 1`] = `
119+
"import { defineComponent as _defineComponent } from 'vue'
120+
121+
export default /*#__PURE__*/_defineComponent({
122+
props: {
123+
foo: { type: Number, required: true, default: 1 },
124+
bar: { type: Number, required: true, default: 2 },
125+
\\"foo:bar\\": { type: String, required: true, default: 'foo-bar' },
126+
\\"onUpdate:modelValue\\": { type: Function, required: true }
127+
},
128+
setup(__props: any) {
129+
130+
131+
132+
return () => {}
133+
}
134+
135+
})"
136+
`;
137+
100138
exports[`sfc reactive props destructure > default values w/ type declaration 1`] = `
101139
"import { defineComponent as _defineComponent } from 'vue'
102140

packages/compiler-sfc/__tests__/compileScript/definePropsDestructure.spec.ts

+53
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,28 @@ describe('sfc reactive props destructure', () => {
106106
})`)
107107
assertCode(content)
108108
})
109+
test('default values w/ runtime declaration & key is string', () => {
110+
const { content, bindings } = compile(`
111+
<script setup>
112+
const { foo = 1, 'foo:bar': fooBar = 'foo-bar' } = defineProps(['foo', 'foo:bar'])
113+
</script>
114+
`)
115+
expect(bindings).toStrictEqual({
116+
__propsAliases: {
117+
fooBar: 'foo:bar'
118+
},
119+
foo: BindingTypes.PROPS,
120+
'foo:bar': BindingTypes.PROPS,
121+
fooBar: BindingTypes.PROPS_ALIASED
122+
})
123+
124+
expect(content).toMatch(`
125+
props: _mergeDefaults(['foo', 'foo:bar'], {
126+
foo: 1,
127+
"foo:bar": 'foo-bar'
128+
}),`)
129+
assertCode(content)
130+
})
109131

110132
test('default values w/ type declaration', () => {
111133
const { content } = compile(`
@@ -123,6 +145,37 @@ describe('sfc reactive props destructure', () => {
123145
assertCode(content)
124146
})
125147

148+
test('default values w/ type declaration & key is string', () => {
149+
const { content, bindings } = compile(`
150+
<script setup lang="ts">
151+
const { foo = 1, bar = 2, 'foo:bar': fooBar = 'foo-bar' } = defineProps<{
152+
"foo": number // double-quoted string
153+
'bar': number // single-quoted string
154+
'foo:bar': string // single-quoted string containing symbols
155+
"onUpdate:modelValue": (val: number) => void // double-quoted string containing symbols
156+
}>()
157+
</script>
158+
`)
159+
expect(bindings).toStrictEqual({
160+
__propsAliases: {
161+
fooBar: 'foo:bar'
162+
},
163+
foo: BindingTypes.PROPS,
164+
bar: BindingTypes.PROPS,
165+
'foo:bar': BindingTypes.PROPS,
166+
fooBar: BindingTypes.PROPS_ALIASED,
167+
'onUpdate:modelValue': BindingTypes.PROPS
168+
})
169+
expect(content).toMatch(`
170+
props: {
171+
foo: { type: Number, required: true, default: 1 },
172+
bar: { type: Number, required: true, default: 2 },
173+
"foo:bar": { type: String, required: true, default: 'foo-bar' },
174+
"onUpdate:modelValue": { type: Function, required: true }
175+
},`)
176+
assertCode(content)
177+
})
178+
126179
test('default values w/ type declaration, prod mode', () => {
127180
const { content } = compile(
128181
`

packages/compiler-sfc/src/script/defineProps.ts

+18-5
Original file line numberDiff line numberDiff line change
@@ -133,10 +133,11 @@ export function genRuntimeProps(ctx: ScriptCompileContext): string | undefined {
133133
const defaults: string[] = []
134134
for (const key in ctx.propsDestructuredBindings) {
135135
const d = genDestructuredDefaultValue(ctx, key)
136+
const finalKey = getEscapedKey(key)
136137
if (d)
137138
defaults.push(
138-
`${key}: ${d.valueString}${
139-
d.needSkipFactory ? `, __skip_${key}: true` : ``
139+
`${finalKey}: ${d.valueString}${
140+
d.needSkipFactory ? `, __skip_${finalKey}: true` : ``
140141
}`
141142
)
142143
}
@@ -248,8 +249,9 @@ function genRuntimePropFromType(
248249
}
249250
}
250251

252+
const finalKey = getEscapedKey(key)
251253
if (!ctx.options.isProd) {
252-
return `${key}: { ${concatStrings([
254+
return `${finalKey}: { ${concatStrings([
253255
`type: ${toRuntimeTypeString(type)}`,
254256
`required: ${required}`,
255257
skipCheck && 'skipCheck: true',
@@ -265,13 +267,13 @@ function genRuntimePropFromType(
265267
// #4783 for boolean, should keep the type
266268
// #7111 for function, if default value exists or it's not static, should keep it
267269
// in production
268-
return `${key}: { ${concatStrings([
270+
return `${finalKey}: { ${concatStrings([
269271
`type: ${toRuntimeTypeString(type)}`,
270272
defaultString
271273
])} }`
272274
} else {
273275
// production: checks are useless
274-
return `${key}: ${defaultString ? `{ ${defaultString} }` : `{}`}`
276+
return `${finalKey}: ${defaultString ? `{ ${defaultString} }` : `{}`}`
275277
}
276278
}
277279

@@ -362,3 +364,14 @@ function inferValueType(node: Node): string | undefined {
362364
return 'Function'
363365
}
364366
}
367+
368+
/**
369+
* key may contain symbols
370+
* e.g. onUpdate:modelValue -> "onUpdate:modelValue"
371+
*/
372+
export const escapeSymbolsRE = /[ !"#$%&'()*+,./:;<=>?@[\\\]^`{|}~]/g
373+
function getEscapedKey(key: string) {
374+
return escapeSymbolsRE.test(key)
375+
? JSON.stringify(key)
376+
: key
377+
}

packages/compiler-sfc/src/style/cssVars.ts

+2-1
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import {
88
BindingMetadata
99
} from '@vue/compiler-dom'
1010
import { SFCDescriptor } from '../parse'
11+
import { escapeSymbolsRE } from '../script/defineProps'
1112
import { PluginCreator } from 'postcss'
1213
import hash from 'hash-sum'
1314

@@ -32,7 +33,7 @@ function genVarName(id: string, raw: string, isProd: boolean): string {
3233
} else {
3334
// escape ASCII Punctuation & Symbols
3435
return `${id}-${raw.replace(
35-
/[ !"#$%&'()*+,./:;<=>?@[\\\]^`{|}~]/g,
36+
escapeSymbolsRE,
3637
s => `\\${s}`
3738
)}`
3839
}

0 commit comments

Comments
 (0)