From b20350ded562d27e5901f308d0bc13344f840c4a Mon Sep 17 00:00:00 2001 From: Evan You Date: Thu, 4 Jan 2024 17:07:29 +0800 Subject: [PATCH] fix(compiler-sfc): fix co-usage of defineModel transform options and props destructure close #9972 --- .../__snapshots__/defineModel.spec.ts.snap | 60 ++++++++++--- .../compileScript/defineModel.spec.ts | 49 ++++++++--- .../compiler-sfc/src/script/defineModel.ts | 88 +++++++++++-------- 3 files changed, 134 insertions(+), 63 deletions(-) diff --git a/packages/compiler-sfc/__tests__/compileScript/__snapshots__/defineModel.spec.ts.snap b/packages/compiler-sfc/__tests__/compileScript/__snapshots__/defineModel.spec.ts.snap index 1163a4c02ba..b6a93541d36 100644 --- a/packages/compiler-sfc/__tests__/compileScript/__snapshots__/defineModel.spec.ts.snap +++ b/packages/compiler-sfc/__tests__/compileScript/__snapshots__/defineModel.spec.ts.snap @@ -17,8 +17,8 @@ export default { __expose(); const modelValue = _useModel(__props, "modelValue") - const c = _useModel(__props, "count") - const toString = _useModel(__props, "toString") + const c = _useModel(__props, 'count') + const toString = _useModel(__props, 'toString') return { modelValue, c, toString } } @@ -40,7 +40,10 @@ export default /*#__PURE__*/_defineComponent({ setup(__props, { expose: __expose }) { __expose(); - const modelValue = _useModel(__props, "modelValue", { get(v) { return v - 1 }, set: (v) => { return v + 1 }, }) + const modelValue = _useModel(__props, "modelValue", { + get(v) { return v - 1 }, + set: (v) => { return v + 1 }, + }) return { modelValue } } @@ -63,7 +66,36 @@ export default /*#__PURE__*/_defineComponent({ setup(__props, { expose: __expose }) { __expose(); - const modelValue = _useModel(__props, "modelValue", { get(v) { return v - 1 }, set: (v) => { return v + 1 }, }) + const modelValue = _useModel(__props, "modelValue", { + get(v) { return v - 1 }, + set: (v) => { return v + 1 }, + }) + +return { modelValue } +} + +})" +`; + +exports[`defineModel() > usage w/ props destructure 1`] = ` +"import { useModel as _useModel, mergeModels as _mergeModels, defineComponent as _defineComponent } from 'vue' + +export default /*#__PURE__*/_defineComponent({ + props: /*#__PURE__*/_mergeModels({ + x: { type: Number, required: true } + }, { + "modelValue": { + }, + "modelModifiers": {}, + }), + emits: ["update:modelValue"], + setup(__props: any, { expose: __expose }) { + __expose(); + + + const modelValue = _useModel(__props, "modelValue", { + set: (v) => { return v + __props.x } + }) return { modelValue } } @@ -84,7 +116,7 @@ export default { __expose(); - const count = _useModel(__props, "count") + const count = _useModel(__props, 'count') return { count } } @@ -132,10 +164,10 @@ export default /*#__PURE__*/_defineComponent({ setup(__props, { expose: __expose }) { __expose(); - const modelValue = _useModel(__props, "modelValue") - const count = _useModel(__props, "count") - const disabled = _useModel(__props, "disabled") - const any = _useModel(__props, "any") + const modelValue = _useModel(__props, "modelValue") + const count = _useModel(__props, 'count') + const disabled = _useModel(__props, 'disabled') + const any = _useModel(__props, 'any') return { modelValue, count, disabled, any } } @@ -163,11 +195,11 @@ export default /*#__PURE__*/_defineComponent({ setup(__props, { expose: __expose }) { __expose(); - const modelValue = _useModel(__props, "modelValue") - const fn = _useModel(__props, "fn") - const fnWithDefault = _useModel(__props, "fnWithDefault") - const str = _useModel(__props, "str") - const optional = _useModel(__props, "optional") + const modelValue = _useModel(__props, "modelValue") + const fn = _useModel<() => void>(__props, 'fn') + const fnWithDefault = _useModel<() => void>(__props, 'fnWithDefault') + const str = _useModel(__props, 'str') + const optional = _useModel(__props, 'optional') return { modelValue, fn, fnWithDefault, str, optional } } diff --git a/packages/compiler-sfc/__tests__/compileScript/defineModel.spec.ts b/packages/compiler-sfc/__tests__/compileScript/defineModel.spec.ts index 6feff4500f1..304258615a8 100644 --- a/packages/compiler-sfc/__tests__/compileScript/defineModel.spec.ts +++ b/packages/compiler-sfc/__tests__/compileScript/defineModel.spec.ts @@ -23,7 +23,8 @@ describe('defineModel()', () => { expect(content).toMatch( `const modelValue = _useModel(__props, "modelValue")`, ) - expect(content).toMatch(`const c = _useModel(__props, "count")`) + expect(content).toMatch(`const c = _useModel(__props, 'count')`) + expect(content).toMatch(`const toString = _useModel(__props, 'toString')`) expect(content).toMatch(`return { modelValue, c, toString }`) expect(content).not.toMatch('defineModel') @@ -71,7 +72,7 @@ describe('defineModel()', () => { "count": {}, "countModifiers": {}, })`) - expect(content).toMatch(`const count = _useModel(__props, "count")`) + expect(content).toMatch(`const count = _useModel(__props, 'count')`) expect(content).not.toMatch('defineModel') expect(bindings).toStrictEqual({ foo: BindingTypes.PROPS, @@ -104,11 +105,15 @@ describe('defineModel()', () => { ) expect(content).toMatch( - `const modelValue = _useModel(__props, "modelValue")`, + `const modelValue = _useModel(__props, "modelValue")`, + ) + expect(content).toMatch(`const count = _useModel(__props, 'count')`) + expect(content).toMatch( + `const disabled = _useModel(__props, 'disabled')`, + ) + expect(content).toMatch( + `const any = _useModel(__props, 'any')`, ) - expect(content).toMatch(`const count = _useModel(__props, "count")`) - expect(content).toMatch(`const disabled = _useModel(__props, "disabled")`) - expect(content).toMatch(`const any = _useModel(__props, "any")`) expect(bindings).toStrictEqual({ modelValue: BindingTypes.SETUP_REF, @@ -143,10 +148,10 @@ describe('defineModel()', () => { 'emits: ["update:modelValue", "update:fn", "update:fnWithDefault", "update:str", "update:optional"]', ) expect(content).toMatch( - `const modelValue = _useModel(__props, "modelValue")`, + `const modelValue = _useModel(__props, "modelValue")`, ) - expect(content).toMatch(`const fn = _useModel(__props, "fn")`) - expect(content).toMatch(`const str = _useModel(__props, "str")`) + expect(content).toMatch(`const fn = _useModel<() => void>(__props, 'fn')`) + expect(content).toMatch(`const str = _useModel(__props, 'str')`) expect(bindings).toStrictEqual({ modelValue: BindingTypes.SETUP_REF, fn: BindingTypes.SETUP_REF, @@ -171,7 +176,10 @@ describe('defineModel()', () => { assertCode(content) expect(content).toMatch(/"modelValue": {\s+required: true,?\s+}/m) expect(content).toMatch( - `_useModel(__props, "modelValue", { get(v) { return v - 1 }, set: (v) => { return v + 1 }, })`, + `_useModel(__props, "modelValue", { + get(v) { return v - 1 }, + set: (v) => { return v + 1 }, + })`, ) const { content: content2 } = compile( @@ -191,7 +199,26 @@ describe('defineModel()', () => { /"modelValue": {\s+default: 0,\s+required: true,?\s+}/m, ) expect(content2).toMatch( - `_useModel(__props, "modelValue", { get(v) { return v - 1 }, set: (v) => { return v + 1 }, })`, + `_useModel(__props, "modelValue", { + get(v) { return v - 1 }, + set: (v) => { return v + 1 }, + })`, ) }) + + test('usage w/ props destructure', () => { + const { content } = compile( + ` + + `, + { propsDestructure: true }, + ) + assertCode(content) + expect(content).toMatch(`set: (v) => { return v + __props.x }`) + }) }) diff --git a/packages/compiler-sfc/src/script/defineModel.ts b/packages/compiler-sfc/src/script/defineModel.ts index 9411fa460b9..b94b7994622 100644 --- a/packages/compiler-sfc/src/script/defineModel.ts +++ b/packages/compiler-sfc/src/script/defineModel.ts @@ -33,7 +33,8 @@ export function processDefineModel( let modelName: string let options: Node | undefined const arg0 = node.arguments[0] && unwrapTSNode(node.arguments[0]) - if (arg0 && arg0.type === 'StringLiteral') { + const hasName = arg0 && arg0.type === 'StringLiteral' + if (hasName) { modelName = arg0.value options = node.arguments[1] } else { @@ -46,39 +47,42 @@ export function processDefineModel( } let optionsString = options && ctx.getString(options) - let runtimeOptions = '' - let transformOptions = '' - - if (options) { - if (options.type === 'ObjectExpression') { - for (let i = options.properties.length - 1; i >= 0; i--) { - const p = options.properties[i] - if (p.type === 'SpreadElement' || p.computed) { - runtimeOptions = optionsString! - break - } - if ( - (p.type === 'ObjectProperty' || p.type === 'ObjectMethod') && - ((p.key.type === 'Identifier' && - (p.key.name === 'get' || p.key.name === 'set')) || - (p.key.type === 'StringLiteral' && - (p.key.value === 'get' || p.key.value === 'set'))) - ) { - transformOptions = ctx.getString(p) + ', ' + transformOptions - - // remove transform option from prop options to avoid duplicates - const offset = p.start! - options.start! - const next = options.properties[i + 1] - const end = (next ? next.start! : options.end! - 1) - options.start! - optionsString = - optionsString.slice(0, offset) + optionsString.slice(end) - } + let optionsRemoved = !options + + if ( + options && + options.type === 'ObjectExpression' && + !options.properties.some(p => p.type === 'SpreadElement' || p.computed) + ) { + let removed = 0 + for (let i = options.properties.length - 1; i >= 0; i--) { + const p = options.properties[i] + const next = options.properties[i + 1] + const start = p.start! + const end = next ? next.start! : options.end! - 1 + if ( + (p.type === 'ObjectProperty' || p.type === 'ObjectMethod') && + ((p.key.type === 'Identifier' && + (p.key.name === 'get' || p.key.name === 'set')) || + (p.key.type === 'StringLiteral' && + (p.key.value === 'get' || p.key.value === 'set'))) + ) { + // remove runtime-only options from prop options to avoid duplicates + optionsString = + optionsString.slice(0, start - options.start!) + + optionsString.slice(end - options.start!) + } else { + // remove prop options from runtime options + removed++ + ctx.s.remove(ctx.startOffset! + start, ctx.startOffset! + end) } - if (!runtimeOptions && transformOptions) { - runtimeOptions = `{ ${transformOptions} }` - } - } else { - runtimeOptions = optionsString! + } + if (removed === options.properties.length) { + optionsRemoved = true + ctx.s.remove( + ctx.startOffset! + (hasName ? arg0.end! : options.start!), + ctx.startOffset! + options.end!, + ) } } @@ -91,12 +95,20 @@ export function processDefineModel( // register binding type ctx.bindingMetadata[modelName] = BindingTypes.PROPS + // defineModel -> useModel ctx.s.overwrite( - ctx.startOffset! + node.start!, - ctx.startOffset! + node.end!, - `${ctx.helper('useModel')}(__props, ${JSON.stringify(modelName)}${ - runtimeOptions ? `, ${runtimeOptions}` : `` - })`, + ctx.startOffset! + node.callee.start!, + ctx.startOffset! + node.callee.end!, + ctx.helper('useModel'), + ) + // inject arguments + ctx.s.appendLeft( + ctx.startOffset! + + (node.arguments.length ? node.arguments[0].start! : node.end! - 1), + `__props, ` + + (hasName + ? `` + : `${JSON.stringify(modelName)}${optionsRemoved ? `` : `, `}`), ) return true