fix(compiler-sfc): handle prop keys that need escaping (#7803)

close #8291
This commit is contained in:
白雾三语 2023-05-12 18:24:59 +08:00 committed by GitHub
parent aa1e77d532
commit 690ef29635
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 111 additions and 6 deletions

View File

@ -97,6 +97,44 @@ return () => {}
}"
`;
exports[`sfc reactive props destructure > default values w/ runtime declaration & key is string 1`] = `
"import { mergeDefaults as _mergeDefaults } from 'vue'
export default {
props: _mergeDefaults(['foo', 'foo:bar'], {
foo: 1,
\\"foo:bar\\": 'foo-bar'
}),
setup(__props) {
return () => {}
}
}"
`;
exports[`sfc reactive props destructure > default values w/ type declaration & key is string 1`] = `
"import { defineComponent as _defineComponent } from 'vue'
export default /*#__PURE__*/_defineComponent({
props: {
foo: { type: Number, required: true, default: 1 },
bar: { type: Number, required: true, default: 2 },
\\"foo:bar\\": { type: String, required: true, default: 'foo-bar' },
\\"onUpdate:modelValue\\": { type: Function, required: true }
},
setup(__props: any) {
return () => {}
}
})"
`;
exports[`sfc reactive props destructure > default values w/ type declaration 1`] = `
"import { defineComponent as _defineComponent } from 'vue'

View File

@ -106,6 +106,28 @@ describe('sfc reactive props destructure', () => {
})`)
assertCode(content)
})
test('default values w/ runtime declaration & key is string', () => {
const { content, bindings } = compile(`
<script setup>
const { foo = 1, 'foo:bar': fooBar = 'foo-bar' } = defineProps(['foo', 'foo:bar'])
</script>
`)
expect(bindings).toStrictEqual({
__propsAliases: {
fooBar: 'foo:bar'
},
foo: BindingTypes.PROPS,
'foo:bar': BindingTypes.PROPS,
fooBar: BindingTypes.PROPS_ALIASED
})
expect(content).toMatch(`
props: _mergeDefaults(['foo', 'foo:bar'], {
foo: 1,
"foo:bar": 'foo-bar'
}),`)
assertCode(content)
})
test('default values w/ type declaration', () => {
const { content } = compile(`
@ -123,6 +145,37 @@ describe('sfc reactive props destructure', () => {
assertCode(content)
})
test('default values w/ type declaration & key is string', () => {
const { content, bindings } = compile(`
<script setup lang="ts">
const { foo = 1, bar = 2, 'foo:bar': fooBar = 'foo-bar' } = defineProps<{
"foo": number // double-quoted string
'bar': number // single-quoted string
'foo:bar': string // single-quoted string containing symbols
"onUpdate:modelValue": (val: number) => void // double-quoted string containing symbols
}>()
</script>
`)
expect(bindings).toStrictEqual({
__propsAliases: {
fooBar: 'foo:bar'
},
foo: BindingTypes.PROPS,
bar: BindingTypes.PROPS,
'foo:bar': BindingTypes.PROPS,
fooBar: BindingTypes.PROPS_ALIASED,
'onUpdate:modelValue': BindingTypes.PROPS
})
expect(content).toMatch(`
props: {
foo: { type: Number, required: true, default: 1 },
bar: { type: Number, required: true, default: 2 },
"foo:bar": { type: String, required: true, default: 'foo-bar' },
"onUpdate:modelValue": { type: Function, required: true }
},`)
assertCode(content)
})
test('default values w/ type declaration, prod mode', () => {
const { content } = compile(
`

View File

@ -133,10 +133,11 @@ export function genRuntimeProps(ctx: ScriptCompileContext): string | undefined {
const defaults: string[] = []
for (const key in ctx.propsDestructuredBindings) {
const d = genDestructuredDefaultValue(ctx, key)
const finalKey = getEscapedKey(key)
if (d)
defaults.push(
`${key}: ${d.valueString}${
d.needSkipFactory ? `, __skip_${key}: true` : ``
`${finalKey}: ${d.valueString}${
d.needSkipFactory ? `, __skip_${finalKey}: true` : ``
}`
)
}
@ -248,8 +249,9 @@ function genRuntimePropFromType(
}
}
const finalKey = getEscapedKey(key)
if (!ctx.options.isProd) {
return `${key}: { ${concatStrings([
return `${finalKey}: { ${concatStrings([
`type: ${toRuntimeTypeString(type)}`,
`required: ${required}`,
skipCheck && 'skipCheck: true',
@ -265,13 +267,13 @@ function genRuntimePropFromType(
// #4783 for boolean, should keep the type
// #7111 for function, if default value exists or it's not static, should keep it
// in production
return `${key}: { ${concatStrings([
return `${finalKey}: { ${concatStrings([
`type: ${toRuntimeTypeString(type)}`,
defaultString
])} }`
} else {
// production: checks are useless
return `${key}: ${defaultString ? `{ ${defaultString} }` : `{}`}`
return `${finalKey}: ${defaultString ? `{ ${defaultString} }` : `{}`}`
}
}
@ -362,3 +364,14 @@ function inferValueType(node: Node): string | undefined {
return 'Function'
}
}
/**
* key may contain symbols
* e.g. onUpdate:modelValue -> "onUpdate:modelValue"
*/
export const escapeSymbolsRE = /[ !"#$%&'()*+,./:;<=>?@[\\\]^`{|}~]/g
function getEscapedKey(key: string) {
return escapeSymbolsRE.test(key)
? JSON.stringify(key)
: key
}

View File

@ -8,6 +8,7 @@ import {
BindingMetadata
} from '@vue/compiler-dom'
import { SFCDescriptor } from '../parse'
import { escapeSymbolsRE } from '../script/defineProps'
import { PluginCreator } from 'postcss'
import hash from 'hash-sum'
@ -32,7 +33,7 @@ function genVarName(id: string, raw: string, isProd: boolean): string {
} else {
// escape ASCII Punctuation & Symbols
return `${id}-${raw.replace(
/[ !"#$%&'()*+,./:;<=>?@[\\\]^`{|}~]/g,
escapeSymbolsRE,
s => `\\${s}`
)}`
}