From 3e08d246dfd8523c54fb8e7a4a6fd5506ffb1bcc Mon Sep 17 00:00:00 2001 From: auvred <61150013+auvred@users.noreply.github.com> Date: Fri, 10 Nov 2023 11:23:47 +0300 Subject: [PATCH] fix(compiler-sfc): consistently escape type-only prop names (#8654) close #8635 close #8910 close vitejs/vite-plugin-vue#184 --- .../__snapshots__/defineProps.spec.ts.snap | 45 +++++++++ .../compileScript/defineProps.spec.ts | 99 +++++++++++++++++++ .../compiler-sfc/src/script/defineProps.ts | 6 +- packages/compiler-sfc/src/script/utils.ts | 12 ++- packages/compiler-sfc/src/style/cssVars.ts | 4 +- 5 files changed, 158 insertions(+), 8 deletions(-) diff --git a/packages/compiler-sfc/__tests__/compileScript/__snapshots__/defineProps.spec.ts.snap b/packages/compiler-sfc/__tests__/compileScript/__snapshots__/defineProps.spec.ts.snap index 5b0f96c5014..158b5c8f555 100644 --- a/packages/compiler-sfc/__tests__/compileScript/__snapshots__/defineProps.spec.ts.snap +++ b/packages/compiler-sfc/__tests__/compileScript/__snapshots__/defineProps.spec.ts.snap @@ -46,6 +46,51 @@ export default /*#__PURE__*/_defineComponent({ const { foo } = __props +return { } +} + +})" +`; + +exports[`defineProps > should escape names w/ special symbols 1`] = ` +"import { defineComponent as _defineComponent } from 'vue' + +export default /*#__PURE__*/_defineComponent({ + props: { + \\"spa ce\\": { type: null, required: true }, + \\"exclamation!mark\\": { type: null, required: true }, + \\"double\\\\\\"quote\\": { type: null, required: true }, + \\"hash#tag\\": { type: null, required: true }, + \\"dollar$sign\\": { type: null, required: true }, + \\"percentage%sign\\": { type: null, required: true }, + \\"amper&sand\\": { type: null, required: true }, + \\"single'quote\\": { type: null, required: true }, + \\"round(brack)ets\\": { type: null, required: true }, + \\"aste*risk\\": { type: null, required: true }, + \\"pl+us\\": { type: null, required: true }, + \\"com,ma\\": { type: null, required: true }, + \\"do.t\\": { type: null, required: true }, + \\"sla/sh\\": { type: null, required: true }, + \\"co:lon\\": { type: null, required: true }, + \\"semi;colon\\": { type: null, required: true }, + \\"angleets\\": { type: null, required: true }, + \\"equal=sign\\": { type: null, required: true }, + \\"question?mark\\": { type: null, required: true }, + \\"at@sign\\": { type: null, required: true }, + \\"square[brack]ets\\": { type: null, required: true }, + \\"back\\\\\\\\slash\\": { type: null, required: true }, + \\"ca^ret\\": { type: null, required: true }, + \\"back\`tick\\": { type: null, required: true }, + \\"curly{bra}ces\\": { type: null, required: true }, + \\"pi|pe\\": { type: null, required: true }, + \\"til~de\\": { type: null, required: true }, + \\"da-sh\\": { type: null, required: true } + }, + setup(__props: any, { expose: __expose }) { + __expose(); + + + return { } } diff --git a/packages/compiler-sfc/__tests__/compileScript/defineProps.spec.ts b/packages/compiler-sfc/__tests__/compileScript/defineProps.spec.ts index 674d697a597..23d6a806f0d 100644 --- a/packages/compiler-sfc/__tests__/compileScript/defineProps.spec.ts +++ b/packages/compiler-sfc/__tests__/compileScript/defineProps.spec.ts @@ -611,4 +611,103 @@ const props = defineProps({ foo: String }) }).toThrow(`cannot accept both type and non-type arguments`) }) }) + + test('should escape names w/ special symbols', () => { + const { content, bindings } = compile(` + `) + assertCode(content) + expect(content).toMatch(`"spa ce": { type: null, required: true }`) + expect(content).toMatch( + `"exclamation!mark": { type: null, required: true }` + ) + expect(content).toMatch(`"double\\"quote": { type: null, required: true }`) + expect(content).toMatch(`"hash#tag": { type: null, required: true }`) + expect(content).toMatch(`"dollar$sign": { type: null, required: true }`) + expect(content).toMatch(`"percentage%sign": { type: null, required: true }`) + expect(content).toMatch(`"amper&sand": { type: null, required: true }`) + expect(content).toMatch(`"single'quote": { type: null, required: true }`) + expect(content).toMatch(`"round(brack)ets": { type: null, required: true }`) + expect(content).toMatch(`"aste*risk": { type: null, required: true }`) + expect(content).toMatch(`"pl+us": { type: null, required: true }`) + expect(content).toMatch(`"com,ma": { type: null, required: true }`) + expect(content).toMatch(`"do.t": { type: null, required: true }`) + expect(content).toMatch(`"sla/sh": { type: null, required: true }`) + expect(content).toMatch(`"co:lon": { type: null, required: true }`) + expect(content).toMatch(`"semi;colon": { type: null, required: true }`) + expect(content).toMatch(`"angleets": { type: null, required: true }`) + expect(content).toMatch(`"equal=sign": { type: null, required: true }`) + expect(content).toMatch(`"question?mark": { type: null, required: true }`) + expect(content).toMatch(`"at@sign": { type: null, required: true }`) + expect(content).toMatch( + `"square[brack]ets": { type: null, required: true }` + ) + expect(content).toMatch(`"back\\\\slash": { type: null, required: true }`) + expect(content).toMatch(`"ca^ret": { type: null, required: true }`) + expect(content).toMatch(`"back\`tick": { type: null, required: true }`) + expect(content).toMatch(`"curly{bra}ces": { type: null, required: true }`) + expect(content).toMatch(`"pi|pe": { type: null, required: true }`) + expect(content).toMatch(`"til~de": { type: null, required: true }`) + expect(content).toMatch(`"da-sh": { type: null, required: true }`) + expect(bindings).toStrictEqual({ + 'spa ce': BindingTypes.PROPS, + 'exclamation!mark': BindingTypes.PROPS, + 'double"quote': BindingTypes.PROPS, + 'hash#tag': BindingTypes.PROPS, + dollar$sign: BindingTypes.PROPS, + 'percentage%sign': BindingTypes.PROPS, + 'amper&sand': BindingTypes.PROPS, + "single'quote": BindingTypes.PROPS, + 'round(brack)ets': BindingTypes.PROPS, + 'aste*risk': BindingTypes.PROPS, + 'pl+us': BindingTypes.PROPS, + 'com,ma': BindingTypes.PROPS, + 'do.t': BindingTypes.PROPS, + 'sla/sh': BindingTypes.PROPS, + 'co:lon': BindingTypes.PROPS, + 'semi;colon': BindingTypes.PROPS, + 'angleets': BindingTypes.PROPS, + 'equal=sign': BindingTypes.PROPS, + 'question?mark': BindingTypes.PROPS, + 'at@sign': BindingTypes.PROPS, + 'square[brack]ets': BindingTypes.PROPS, + 'back\\slash': BindingTypes.PROPS, + 'ca^ret': BindingTypes.PROPS, + 'back`tick': BindingTypes.PROPS, + 'curly{bra}ces': BindingTypes.PROPS, + 'pi|pe': BindingTypes.PROPS, + 'til~de': BindingTypes.PROPS, + 'da-sh': BindingTypes.PROPS + }) + }) }) diff --git a/packages/compiler-sfc/src/script/defineProps.ts b/packages/compiler-sfc/src/script/defineProps.ts index 5004e314da1..9de15b92b76 100644 --- a/packages/compiler-sfc/src/script/defineProps.ts +++ b/packages/compiler-sfc/src/script/defineProps.ts @@ -17,7 +17,7 @@ import { isCallOf, unwrapTSNode, toRuntimeTypeString, - getEscapedKey + getEscapedPropName } from './utils' import { genModelProps } from './defineModel' import { getObjectOrArrayExpressionKeys } from './analyzeScriptBindings' @@ -135,7 +135,7 @@ 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) + const finalKey = getEscapedPropName(key) if (d) defaults.push( `${finalKey}: ${d.valueString}${ @@ -251,7 +251,7 @@ function genRuntimePropFromType( } } - const finalKey = getEscapedKey(key) + const finalKey = getEscapedPropName(key) if (!ctx.options.isProd) { return `${finalKey}: { ${concatStrings([ `type: ${toRuntimeTypeString(type)}`, diff --git a/packages/compiler-sfc/src/script/utils.ts b/packages/compiler-sfc/src/script/utils.ts index 42c4718e3a8..8afadf63ee4 100644 --- a/packages/compiler-sfc/src/script/utils.ts +++ b/packages/compiler-sfc/src/script/utils.ts @@ -113,8 +113,14 @@ export const joinPaths = (path.posix || path).join * key may contain symbols * e.g. onUpdate:modelValue -> "onUpdate:modelValue" */ -export const escapeSymbolsRE = /[ !"#$%&'()*+,./:;<=>?@[\\\]^`{|}~]/g +export const propNameEscapeSymbolsRE = /[ !"#$%&'()*+,./:;<=>?@[\\\]^`{|}~\-]/ -export function getEscapedKey(key: string) { - return escapeSymbolsRE.test(key) ? JSON.stringify(key) : key +export function getEscapedPropName(key: string) { + return propNameEscapeSymbolsRE.test(key) ? JSON.stringify(key) : key +} + +export const cssVarNameEscapeSymbolsRE = /[ !"#$%&'()*+,./:;<=>?@[\\\]^`{|}~]/g + +export function getEscapedCssVarName(key: string) { + return key.replace(cssVarNameEscapeSymbolsRE, s => `\\${s}`) } diff --git a/packages/compiler-sfc/src/style/cssVars.ts b/packages/compiler-sfc/src/style/cssVars.ts index 2380959b819..9fe727bc5dc 100644 --- a/packages/compiler-sfc/src/style/cssVars.ts +++ b/packages/compiler-sfc/src/style/cssVars.ts @@ -8,7 +8,7 @@ import { BindingMetadata } from '@vue/compiler-dom' import { SFCDescriptor } from '../parse' -import { escapeSymbolsRE } from '../script/utils' +import { getEscapedCssVarName } from '../script/utils' import { PluginCreator } from 'postcss' import hash from 'hash-sum' @@ -32,7 +32,7 @@ function genVarName(id: string, raw: string, isProd: boolean): string { return hash(id + raw) } else { // escape ASCII Punctuation & Symbols - return `${id}-${raw.replace(escapeSymbolsRE, s => `\\${s}`)}` + return `${id}-${getEscapedCssVarName(raw)}` } }