Skip to content

Commit 3e08d24

Browse files
authored
fix(compiler-sfc): consistently escape type-only prop names (#8654)
close #8635 close #8910 close vitejs/vite-plugin-vue#184
1 parent 9e1b74b commit 3e08d24

File tree

5 files changed

+158
-8
lines changed

5 files changed

+158
-8
lines changed

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

+45
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,51 @@ export default /*#__PURE__*/_defineComponent({
4646
4747
const { foo } = __props
4848
49+
return { }
50+
}
51+
52+
})"
53+
`;
54+
55+
exports[`defineProps > should escape names w/ special symbols 1`] = `
56+
"import { defineComponent as _defineComponent } from 'vue'
57+
58+
export default /*#__PURE__*/_defineComponent({
59+
props: {
60+
\\"spa ce\\": { type: null, required: true },
61+
\\"exclamation!mark\\": { type: null, required: true },
62+
\\"double\\\\\\"quote\\": { type: null, required: true },
63+
\\"hash#tag\\": { type: null, required: true },
64+
\\"dollar$sign\\": { type: null, required: true },
65+
\\"percentage%sign\\": { type: null, required: true },
66+
\\"amper&sand\\": { type: null, required: true },
67+
\\"single'quote\\": { type: null, required: true },
68+
\\"round(brack)ets\\": { type: null, required: true },
69+
\\"aste*risk\\": { type: null, required: true },
70+
\\"pl+us\\": { type: null, required: true },
71+
\\"com,ma\\": { type: null, required: true },
72+
\\"do.t\\": { type: null, required: true },
73+
\\"sla/sh\\": { type: null, required: true },
74+
\\"co:lon\\": { type: null, required: true },
75+
\\"semi;colon\\": { type: null, required: true },
76+
\\"angle<brack>ets\\": { type: null, required: true },
77+
\\"equal=sign\\": { type: null, required: true },
78+
\\"question?mark\\": { type: null, required: true },
79+
\\"at@sign\\": { type: null, required: true },
80+
\\"square[brack]ets\\": { type: null, required: true },
81+
\\"back\\\\\\\\slash\\": { type: null, required: true },
82+
\\"ca^ret\\": { type: null, required: true },
83+
\\"back\`tick\\": { type: null, required: true },
84+
\\"curly{bra}ces\\": { type: null, required: true },
85+
\\"pi|pe\\": { type: null, required: true },
86+
\\"til~de\\": { type: null, required: true },
87+
\\"da-sh\\": { type: null, required: true }
88+
},
89+
setup(__props: any, { expose: __expose }) {
90+
__expose();
91+
92+
93+
4994
return { }
5095
}
5196

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

+99
Original file line numberDiff line numberDiff line change
@@ -611,4 +611,103 @@ const props = defineProps({ foo: String })
611611
}).toThrow(`cannot accept both type and non-type arguments`)
612612
})
613613
})
614+
615+
test('should escape names w/ special symbols', () => {
616+
const { content, bindings } = compile(`
617+
<script setup lang="ts">
618+
defineProps<{
619+
'spa ce': unknown
620+
'exclamation!mark': unknown
621+
'double"quote': unknown
622+
'hash#tag': unknown
623+
'dollar$sign': unknown
624+
'percentage%sign': unknown
625+
'amper&sand': unknown
626+
"single'quote": unknown
627+
'round(brack)ets': unknown
628+
'aste*risk': unknown
629+
'pl+us': unknown
630+
'com,ma': unknown
631+
'do.t': unknown
632+
'sla/sh': unknown
633+
'co:lon': unknown
634+
'semi;colon': unknown
635+
'angle<brack>ets': unknown
636+
'equal=sign': unknown
637+
'question?mark': unknown
638+
'at@sign': unknown
639+
'square[brack]ets': unknown
640+
'back\\\\slash': unknown
641+
'ca^ret': unknown
642+
'back\`tick': unknown
643+
'curly{bra}ces': unknown
644+
'pi|pe': unknown
645+
'til~de': unknown
646+
'da-sh': unknown
647+
}>()
648+
</script>`)
649+
assertCode(content)
650+
expect(content).toMatch(`"spa ce": { type: null, required: true }`)
651+
expect(content).toMatch(
652+
`"exclamation!mark": { type: null, required: true }`
653+
)
654+
expect(content).toMatch(`"double\\"quote": { type: null, required: true }`)
655+
expect(content).toMatch(`"hash#tag": { type: null, required: true }`)
656+
expect(content).toMatch(`"dollar$sign": { type: null, required: true }`)
657+
expect(content).toMatch(`"percentage%sign": { type: null, required: true }`)
658+
expect(content).toMatch(`"amper&sand": { type: null, required: true }`)
659+
expect(content).toMatch(`"single'quote": { type: null, required: true }`)
660+
expect(content).toMatch(`"round(brack)ets": { type: null, required: true }`)
661+
expect(content).toMatch(`"aste*risk": { type: null, required: true }`)
662+
expect(content).toMatch(`"pl+us": { type: null, required: true }`)
663+
expect(content).toMatch(`"com,ma": { type: null, required: true }`)
664+
expect(content).toMatch(`"do.t": { type: null, required: true }`)
665+
expect(content).toMatch(`"sla/sh": { type: null, required: true }`)
666+
expect(content).toMatch(`"co:lon": { type: null, required: true }`)
667+
expect(content).toMatch(`"semi;colon": { type: null, required: true }`)
668+
expect(content).toMatch(`"angle<brack>ets": { type: null, required: true }`)
669+
expect(content).toMatch(`"equal=sign": { type: null, required: true }`)
670+
expect(content).toMatch(`"question?mark": { type: null, required: true }`)
671+
expect(content).toMatch(`"at@sign": { type: null, required: true }`)
672+
expect(content).toMatch(
673+
`"square[brack]ets": { type: null, required: true }`
674+
)
675+
expect(content).toMatch(`"back\\\\slash": { type: null, required: true }`)
676+
expect(content).toMatch(`"ca^ret": { type: null, required: true }`)
677+
expect(content).toMatch(`"back\`tick": { type: null, required: true }`)
678+
expect(content).toMatch(`"curly{bra}ces": { type: null, required: true }`)
679+
expect(content).toMatch(`"pi|pe": { type: null, required: true }`)
680+
expect(content).toMatch(`"til~de": { type: null, required: true }`)
681+
expect(content).toMatch(`"da-sh": { type: null, required: true }`)
682+
expect(bindings).toStrictEqual({
683+
'spa ce': BindingTypes.PROPS,
684+
'exclamation!mark': BindingTypes.PROPS,
685+
'double"quote': BindingTypes.PROPS,
686+
'hash#tag': BindingTypes.PROPS,
687+
dollar$sign: BindingTypes.PROPS,
688+
'percentage%sign': BindingTypes.PROPS,
689+
'amper&sand': BindingTypes.PROPS,
690+
"single'quote": BindingTypes.PROPS,
691+
'round(brack)ets': BindingTypes.PROPS,
692+
'aste*risk': BindingTypes.PROPS,
693+
'pl+us': BindingTypes.PROPS,
694+
'com,ma': BindingTypes.PROPS,
695+
'do.t': BindingTypes.PROPS,
696+
'sla/sh': BindingTypes.PROPS,
697+
'co:lon': BindingTypes.PROPS,
698+
'semi;colon': BindingTypes.PROPS,
699+
'angle<brack>ets': BindingTypes.PROPS,
700+
'equal=sign': BindingTypes.PROPS,
701+
'question?mark': BindingTypes.PROPS,
702+
'at@sign': BindingTypes.PROPS,
703+
'square[brack]ets': BindingTypes.PROPS,
704+
'back\\slash': BindingTypes.PROPS,
705+
'ca^ret': BindingTypes.PROPS,
706+
'back`tick': BindingTypes.PROPS,
707+
'curly{bra}ces': BindingTypes.PROPS,
708+
'pi|pe': BindingTypes.PROPS,
709+
'til~de': BindingTypes.PROPS,
710+
'da-sh': BindingTypes.PROPS
711+
})
712+
})
614713
})

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

+3-3
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ import {
1717
isCallOf,
1818
unwrapTSNode,
1919
toRuntimeTypeString,
20-
getEscapedKey
20+
getEscapedPropName
2121
} from './utils'
2222
import { genModelProps } from './defineModel'
2323
import { getObjectOrArrayExpressionKeys } from './analyzeScriptBindings'
@@ -135,7 +135,7 @@ export function genRuntimeProps(ctx: ScriptCompileContext): string | undefined {
135135
const defaults: string[] = []
136136
for (const key in ctx.propsDestructuredBindings) {
137137
const d = genDestructuredDefaultValue(ctx, key)
138-
const finalKey = getEscapedKey(key)
138+
const finalKey = getEscapedPropName(key)
139139
if (d)
140140
defaults.push(
141141
`${finalKey}: ${d.valueString}${
@@ -251,7 +251,7 @@ function genRuntimePropFromType(
251251
}
252252
}
253253

254-
const finalKey = getEscapedKey(key)
254+
const finalKey = getEscapedPropName(key)
255255
if (!ctx.options.isProd) {
256256
return `${finalKey}: { ${concatStrings([
257257
`type: ${toRuntimeTypeString(type)}`,

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

+9-3
Original file line numberDiff line numberDiff line change
@@ -113,8 +113,14 @@ export const joinPaths = (path.posix || path).join
113113
* key may contain symbols
114114
* e.g. onUpdate:modelValue -> "onUpdate:modelValue"
115115
*/
116-
export const escapeSymbolsRE = /[ !"#$%&'()*+,./:;<=>?@[\\\]^`{|}~]/g
116+
export const propNameEscapeSymbolsRE = /[ !"#$%&'()*+,./:;<=>?@[\\\]^`{|}~\-]/
117117

118-
export function getEscapedKey(key: string) {
119-
return escapeSymbolsRE.test(key) ? JSON.stringify(key) : key
118+
export function getEscapedPropName(key: string) {
119+
return propNameEscapeSymbolsRE.test(key) ? JSON.stringify(key) : key
120+
}
121+
122+
export const cssVarNameEscapeSymbolsRE = /[ !"#$%&'()*+,./:;<=>?@[\\\]^`{|}~]/g
123+
124+
export function getEscapedCssVarName(key: string) {
125+
return key.replace(cssVarNameEscapeSymbolsRE, s => `\\${s}`)
120126
}

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

+2-2
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import {
88
BindingMetadata
99
} from '@vue/compiler-dom'
1010
import { SFCDescriptor } from '../parse'
11-
import { escapeSymbolsRE } from '../script/utils'
11+
import { getEscapedCssVarName } from '../script/utils'
1212
import { PluginCreator } from 'postcss'
1313
import hash from 'hash-sum'
1414

@@ -32,7 +32,7 @@ function genVarName(id: string, raw: string, isProd: boolean): string {
3232
return hash(id + raw)
3333
} else {
3434
// escape ASCII Punctuation & Symbols
35-
return `${id}-${raw.replace(escapeSymbolsRE, s => `\\${s}`)}`
35+
return `${id}-${getEscapedCssVarName(raw)}`
3636
}
3737
}
3838

0 commit comments

Comments
 (0)