feat(sfc): introduce `defineModel` macro and `useModel` helper (#8018)
This commit is contained in:
parent
3b02c27e6d
commit
14f3d747a3
|
@ -653,6 +653,100 @@ return { }
|
||||||
}"
|
}"
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
exports[`SFC compile <script setup> > defineModel() > basic usage 1`] = `
|
||||||
|
"import { useModel as _useModel } from 'vue'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
props: {
|
||||||
|
\\"modelValue\\": { required: true },
|
||||||
|
\\"count\\": {},
|
||||||
|
},
|
||||||
|
emits: [\\"update:modelValue\\", \\"update:count\\"],
|
||||||
|
setup(__props, { expose: __expose }) {
|
||||||
|
__expose();
|
||||||
|
|
||||||
|
const modelValue = _useModel(__props, \\"modelValue\\")
|
||||||
|
const c = _useModel(__props, \\"count\\")
|
||||||
|
|
||||||
|
return { modelValue, c }
|
||||||
|
}
|
||||||
|
|
||||||
|
}"
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`SFC compile <script setup> > defineModel() > w/ array props 1`] = `
|
||||||
|
"import { useModel as _useModel, mergeModels as _mergeModels } from 'vue'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
props: _mergeModels(['foo', 'bar'], {
|
||||||
|
\\"count\\": {},
|
||||||
|
}),
|
||||||
|
emits: [\\"update:count\\"],
|
||||||
|
setup(__props, { expose: __expose }) {
|
||||||
|
__expose();
|
||||||
|
|
||||||
|
|
||||||
|
const count = _useModel(__props, \\"count\\")
|
||||||
|
|
||||||
|
return { count }
|
||||||
|
}
|
||||||
|
|
||||||
|
}"
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`SFC compile <script setup> > defineModel() > w/ defineProps and defineEmits 1`] = `
|
||||||
|
"import { useModel as _useModel, mergeModels as _mergeModels } from 'vue'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
props: _mergeModels({ foo: String }, {
|
||||||
|
\\"modelValue\\": { default: 0 },
|
||||||
|
}),
|
||||||
|
emits: _mergeModels(['change'], [\\"update:modelValue\\"]),
|
||||||
|
setup(__props, { expose: __expose }) {
|
||||||
|
__expose();
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
const count = _useModel(__props, \\"modelValue\\")
|
||||||
|
|
||||||
|
return { count }
|
||||||
|
}
|
||||||
|
|
||||||
|
}"
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`SFC compile <script setup> > defineModel() > w/ local flag 1`] = `
|
||||||
|
"import { useModel as _useModel } from 'vue'
|
||||||
|
const local = true
|
||||||
|
|
||||||
|
export default {
|
||||||
|
props: {
|
||||||
|
\\"modelValue\\": { local: true, default: 1 },
|
||||||
|
\\"bar\\": { [key]: true },
|
||||||
|
\\"baz\\": { ...x },
|
||||||
|
\\"qux\\": x,
|
||||||
|
\\"foo2\\": { local: true, ...x },
|
||||||
|
\\"hoist\\": { local },
|
||||||
|
},
|
||||||
|
emits: [\\"update:modelValue\\", \\"update:bar\\", \\"update:baz\\", \\"update:qux\\", \\"update:foo2\\", \\"update:hoist\\"],
|
||||||
|
setup(__props, { expose: __expose }) {
|
||||||
|
__expose();
|
||||||
|
|
||||||
|
const foo = _useModel(__props, \\"modelValue\\", { local: true })
|
||||||
|
const bar = _useModel(__props, \\"bar\\", { [key]: true })
|
||||||
|
const baz = _useModel(__props, \\"baz\\", { ...x })
|
||||||
|
const qux = _useModel(__props, \\"qux\\", x)
|
||||||
|
|
||||||
|
const foo2 = _useModel(__props, \\"foo2\\", { local: true })
|
||||||
|
|
||||||
|
const hoist = _useModel(__props, \\"hoist\\", { local })
|
||||||
|
|
||||||
|
return { foo, bar, baz, qux, foo2, local, hoist }
|
||||||
|
}
|
||||||
|
|
||||||
|
}"
|
||||||
|
`;
|
||||||
|
|
||||||
exports[`SFC compile <script setup> > defineOptions() > basic usage 1`] = `
|
exports[`SFC compile <script setup> > defineOptions() > basic usage 1`] = `
|
||||||
"export default /*#__PURE__*/Object.assign({ name: 'FooApp' }, {
|
"export default /*#__PURE__*/Object.assign({ name: 'FooApp' }, {
|
||||||
setup(__props, { expose: __expose }) {
|
setup(__props, { expose: __expose }) {
|
||||||
|
@ -1596,6 +1690,58 @@ return { emit }
|
||||||
})"
|
})"
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
exports[`SFC compile <script setup> > with TypeScript > defineModel() > basic usage 1`] = `
|
||||||
|
"import { useModel as _useModel, defineComponent as _defineComponent } from 'vue'
|
||||||
|
|
||||||
|
export default /*#__PURE__*/_defineComponent({
|
||||||
|
props: {
|
||||||
|
\\"modelValue\\": { type: [Boolean, String] },
|
||||||
|
\\"count\\": { type: Number },
|
||||||
|
\\"disabled\\": { type: Number, ...{ required: false } },
|
||||||
|
\\"any\\": { type: Boolean, skipCheck: true },
|
||||||
|
},
|
||||||
|
emits: [\\"update:modelValue\\", \\"update:count\\", \\"update:disabled\\", \\"update:any\\"],
|
||||||
|
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\\")
|
||||||
|
|
||||||
|
return { modelValue, count, disabled, any }
|
||||||
|
}
|
||||||
|
|
||||||
|
})"
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`SFC compile <script setup> > with TypeScript > defineModel() > w/ production mode 1`] = `
|
||||||
|
"import { useModel as _useModel, defineComponent as _defineComponent } from 'vue'
|
||||||
|
|
||||||
|
export default /*#__PURE__*/_defineComponent({
|
||||||
|
props: {
|
||||||
|
\\"modelValue\\": { type: Boolean },
|
||||||
|
\\"fn\\": {},
|
||||||
|
\\"fnWithDefault\\": { type: Function, ...{ default: () => null } },
|
||||||
|
\\"str\\": {},
|
||||||
|
\\"optional\\": { required: false },
|
||||||
|
},
|
||||||
|
emits: [\\"update:modelValue\\", \\"update:fn\\", \\"update:fnWithDefault\\", \\"update:str\\", \\"update:optional\\"],
|
||||||
|
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\\")
|
||||||
|
|
||||||
|
return { modelValue, fn, fnWithDefault, str, optional }
|
||||||
|
}
|
||||||
|
|
||||||
|
})"
|
||||||
|
`;
|
||||||
|
|
||||||
exports[`SFC compile <script setup> > with TypeScript > defineProps w/ TS assertion 1`] = `
|
exports[`SFC compile <script setup> > with TypeScript > defineProps w/ TS assertion 1`] = `
|
||||||
"import { defineComponent as _defineComponent } from 'vue'
|
"import { defineComponent as _defineComponent } from 'vue'
|
||||||
|
|
||||||
|
@ -1961,7 +2107,7 @@ export default /*#__PURE__*/_defineComponent({
|
||||||
foo: { type: Function },
|
foo: { type: Function },
|
||||||
bar: { type: Boolean },
|
bar: { type: Boolean },
|
||||||
baz: { type: [Boolean, Function] },
|
baz: { type: [Boolean, Function] },
|
||||||
qux: null
|
qux: {}
|
||||||
}, { ...defaults }),
|
}, { ...defaults }),
|
||||||
setup(__props: any, { expose: __expose }) {
|
setup(__props: any, { expose: __expose }) {
|
||||||
__expose();
|
__expose();
|
||||||
|
@ -2063,7 +2209,7 @@ exports[`SFC compile <script setup> > with TypeScript > withDefaults (static) w/
|
||||||
|
|
||||||
export default /*#__PURE__*/_defineComponent({
|
export default /*#__PURE__*/_defineComponent({
|
||||||
props: {
|
props: {
|
||||||
foo: null,
|
foo: {},
|
||||||
bar: { type: Boolean },
|
bar: { type: Boolean },
|
||||||
baz: { type: [Boolean, Function], default: true },
|
baz: { type: [Boolean, Function], default: true },
|
||||||
qux: { default: 'hi' }
|
qux: { default: 'hi' }
|
||||||
|
|
|
@ -123,7 +123,7 @@ export default /*#__PURE__*/_defineComponent({
|
||||||
props: {
|
props: {
|
||||||
foo: { default: 1 },
|
foo: { default: 1 },
|
||||||
bar: { default: () => ({}) },
|
bar: { default: () => ({}) },
|
||||||
baz: null,
|
baz: {},
|
||||||
boola: { type: Boolean },
|
boola: { type: Boolean },
|
||||||
boolb: { type: [Boolean, Number] },
|
boolb: { type: [Boolean, Number] },
|
||||||
func: { type: Function, default: () => {} }
|
func: { type: Function, default: () => {} }
|
||||||
|
|
|
@ -306,6 +306,48 @@ const myEmit = defineEmits(['foo', 'bar'])
|
||||||
'[@vue/compiler-sfc] defineOptions() cannot be used to declare props. Use defineProps() instead.'
|
'[@vue/compiler-sfc] defineOptions() cannot be used to declare props. Use defineProps() instead.'
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('should emit an error with declaring props/emits/slots/expose', () => {
|
||||||
|
expect(() =>
|
||||||
|
compile(`
|
||||||
|
<script setup>
|
||||||
|
defineOptions({ props: ['foo'] })
|
||||||
|
</script>
|
||||||
|
`)
|
||||||
|
).toThrowError(
|
||||||
|
'[@vue/compiler-sfc] defineOptions() cannot be used to declare props. Use defineProps() instead'
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(() =>
|
||||||
|
compile(`
|
||||||
|
<script setup>
|
||||||
|
defineOptions({ emits: ['update'] })
|
||||||
|
</script>
|
||||||
|
`)
|
||||||
|
).toThrowError(
|
||||||
|
'[@vue/compiler-sfc] defineOptions() cannot be used to declare emits. Use defineEmits() instead'
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(() =>
|
||||||
|
compile(`
|
||||||
|
<script setup>
|
||||||
|
defineOptions({ expose: ['foo'] })
|
||||||
|
</script>
|
||||||
|
`)
|
||||||
|
).toThrowError(
|
||||||
|
'[@vue/compiler-sfc] defineOptions() cannot be used to declare expose. Use defineExpose() instead'
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(() =>
|
||||||
|
compile(`
|
||||||
|
<script setup lang="ts">
|
||||||
|
defineOptions({ slots: Object })
|
||||||
|
</script>
|
||||||
|
`)
|
||||||
|
).toThrowError(
|
||||||
|
'[@vue/compiler-sfc] defineOptions() cannot be used to declare slots. Use defineSlots() instead'
|
||||||
|
)
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
test('defineExpose()', () => {
|
test('defineExpose()', () => {
|
||||||
|
@ -323,6 +365,109 @@ defineExpose({ foo: 123 })
|
||||||
expect(content).toMatch(/\b__expose\(\{ foo: 123 \}\)/)
|
expect(content).toMatch(/\b__expose\(\{ foo: 123 \}\)/)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
describe('defineModel()', () => {
|
||||||
|
test('basic usage', () => {
|
||||||
|
const { content, bindings } = compile(
|
||||||
|
`
|
||||||
|
<script setup>
|
||||||
|
const modelValue = defineModel({ required: true })
|
||||||
|
const c = defineModel('count')
|
||||||
|
</script>
|
||||||
|
`,
|
||||||
|
{ defineModel: true }
|
||||||
|
)
|
||||||
|
assertCode(content)
|
||||||
|
expect(content).toMatch('props: {')
|
||||||
|
expect(content).toMatch('"modelValue": { required: true },')
|
||||||
|
expect(content).toMatch('"count": {},')
|
||||||
|
expect(content).toMatch('emits: ["update:modelValue", "update:count"],')
|
||||||
|
expect(content).toMatch(
|
||||||
|
`const modelValue = _useModel(__props, "modelValue")`
|
||||||
|
)
|
||||||
|
expect(content).toMatch(`const c = _useModel(__props, "count")`)
|
||||||
|
expect(content).toMatch(`return { modelValue, c }`)
|
||||||
|
expect(content).not.toMatch('defineModel')
|
||||||
|
|
||||||
|
expect(bindings).toStrictEqual({
|
||||||
|
modelValue: BindingTypes.SETUP_REF,
|
||||||
|
count: BindingTypes.PROPS,
|
||||||
|
c: BindingTypes.SETUP_REF
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
test('w/ defineProps and defineEmits', () => {
|
||||||
|
const { content, bindings } = compile(
|
||||||
|
`
|
||||||
|
<script setup>
|
||||||
|
defineProps({ foo: String })
|
||||||
|
defineEmits(['change'])
|
||||||
|
const count = defineModel({ default: 0 })
|
||||||
|
</script>
|
||||||
|
`,
|
||||||
|
{ defineModel: true }
|
||||||
|
)
|
||||||
|
assertCode(content)
|
||||||
|
expect(content).toMatch(`props: _mergeModels({ foo: String }`)
|
||||||
|
expect(content).toMatch(`"modelValue": { default: 0 }`)
|
||||||
|
expect(content).toMatch(`const count = _useModel(__props, "modelValue")`)
|
||||||
|
expect(content).not.toMatch('defineModel')
|
||||||
|
expect(bindings).toStrictEqual({
|
||||||
|
count: BindingTypes.SETUP_REF,
|
||||||
|
foo: BindingTypes.PROPS,
|
||||||
|
modelValue: BindingTypes.PROPS
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
test('w/ array props', () => {
|
||||||
|
const { content, bindings } = compile(
|
||||||
|
`
|
||||||
|
<script setup>
|
||||||
|
defineProps(['foo', 'bar'])
|
||||||
|
const count = defineModel('count')
|
||||||
|
</script>
|
||||||
|
`,
|
||||||
|
{ defineModel: true }
|
||||||
|
)
|
||||||
|
assertCode(content)
|
||||||
|
expect(content).toMatch(`props: _mergeModels(['foo', 'bar'], {
|
||||||
|
"count": {},
|
||||||
|
})`)
|
||||||
|
expect(content).toMatch(`const count = _useModel(__props, "count")`)
|
||||||
|
expect(content).not.toMatch('defineModel')
|
||||||
|
expect(bindings).toStrictEqual({
|
||||||
|
foo: BindingTypes.PROPS,
|
||||||
|
bar: BindingTypes.PROPS,
|
||||||
|
count: BindingTypes.SETUP_REF
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
test('w/ local flag', () => {
|
||||||
|
const { content } = compile(
|
||||||
|
`<script setup>
|
||||||
|
const foo = defineModel({ local: true, default: 1 })
|
||||||
|
const bar = defineModel('bar', { [key]: true })
|
||||||
|
const baz = defineModel('baz', { ...x })
|
||||||
|
const qux = defineModel('qux', x)
|
||||||
|
|
||||||
|
const foo2 = defineModel('foo2', { local: true, ...x })
|
||||||
|
|
||||||
|
const local = true
|
||||||
|
const hoist = defineModel('hoist', { local })
|
||||||
|
</script>`,
|
||||||
|
{ defineModel: true }
|
||||||
|
)
|
||||||
|
assertCode(content)
|
||||||
|
expect(content).toMatch(
|
||||||
|
`_useModel(__props, "modelValue", { local: true })`
|
||||||
|
)
|
||||||
|
expect(content).toMatch(`_useModel(__props, "bar", { [key]: true })`)
|
||||||
|
expect(content).toMatch(`_useModel(__props, "baz", { ...x })`)
|
||||||
|
expect(content).toMatch(`_useModel(__props, "qux", x)`)
|
||||||
|
expect(content).toMatch(`_useModel(__props, "foo2", { local: true })`)
|
||||||
|
expect(content).toMatch(`_useModel(__props, "hoist", { local })`)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
test('<script> after <script setup> the script content not end with `\\n`', () => {
|
test('<script> after <script setup> the script content not end with `\\n`', () => {
|
||||||
const { content } = compile(`
|
const { content } = compile(`
|
||||||
<script setup>
|
<script setup>
|
||||||
|
@ -1391,7 +1536,7 @@ const emit = defineEmits(['a', 'b'])
|
||||||
expect(content).toMatch(`const props = __props`)
|
expect(content).toMatch(`const props = __props`)
|
||||||
|
|
||||||
// foo has no default value, the Function can be dropped
|
// foo has no default value, the Function can be dropped
|
||||||
expect(content).toMatch(`foo: null`)
|
expect(content).toMatch(`foo: {}`)
|
||||||
expect(content).toMatch(`bar: { type: Boolean }`)
|
expect(content).toMatch(`bar: { type: Boolean }`)
|
||||||
expect(content).toMatch(
|
expect(content).toMatch(
|
||||||
`baz: { type: [Boolean, Function], default: true }`
|
`baz: { type: [Boolean, Function], default: true }`
|
||||||
|
@ -1469,7 +1614,7 @@ const emit = defineEmits(['a', 'b'])
|
||||||
foo: { type: Function },
|
foo: { type: Function },
|
||||||
bar: { type: Boolean },
|
bar: { type: Boolean },
|
||||||
baz: { type: [Boolean, Function] },
|
baz: { type: [Boolean, Function] },
|
||||||
qux: null
|
qux: {}
|
||||||
}, { ...defaults })`.trim()
|
}, { ...defaults })`.trim()
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
@ -1679,6 +1824,86 @@ const emit = defineEmits(['a', 'b'])
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
describe('defineModel()', () => {
|
||||||
|
test('basic usage', () => {
|
||||||
|
const { content, bindings } = compile(
|
||||||
|
`
|
||||||
|
<script setup lang="ts">
|
||||||
|
const modelValue = defineModel<boolean | string>()
|
||||||
|
const count = defineModel<number>('count')
|
||||||
|
const disabled = defineModel<number>('disabled', { required: false })
|
||||||
|
const any = defineModel<any | boolean>('any')
|
||||||
|
</script>
|
||||||
|
`,
|
||||||
|
{ defineModel: true }
|
||||||
|
)
|
||||||
|
assertCode(content)
|
||||||
|
expect(content).toMatch('"modelValue": { type: [Boolean, String] }')
|
||||||
|
expect(content).toMatch('"count": { type: Number }')
|
||||||
|
expect(content).toMatch(
|
||||||
|
'"disabled": { type: Number, ...{ required: false } }'
|
||||||
|
)
|
||||||
|
expect(content).toMatch('"any": { type: Boolean, skipCheck: true }')
|
||||||
|
expect(content).toMatch(
|
||||||
|
'emits: ["update:modelValue", "update:count", "update:disabled", "update:any"]'
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(content).toMatch(
|
||||||
|
`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(bindings).toStrictEqual({
|
||||||
|
modelValue: BindingTypes.SETUP_REF,
|
||||||
|
count: BindingTypes.SETUP_REF,
|
||||||
|
disabled: BindingTypes.SETUP_REF,
|
||||||
|
any: BindingTypes.SETUP_REF
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
test('w/ production mode', () => {
|
||||||
|
const { content, bindings } = compile(
|
||||||
|
`
|
||||||
|
<script setup lang="ts">
|
||||||
|
const modelValue = defineModel<boolean>()
|
||||||
|
const fn = defineModel<() => void>('fn')
|
||||||
|
const fnWithDefault = defineModel<() => void>('fnWithDefault', { default: () => null })
|
||||||
|
const str = defineModel<string>('str')
|
||||||
|
const optional = defineModel<string>('optional', { required: false })
|
||||||
|
</script>
|
||||||
|
`,
|
||||||
|
{ defineModel: true, isProd: true }
|
||||||
|
)
|
||||||
|
assertCode(content)
|
||||||
|
expect(content).toMatch('"modelValue": { type: Boolean }')
|
||||||
|
expect(content).toMatch('"fn": {}')
|
||||||
|
expect(content).toMatch(
|
||||||
|
'"fnWithDefault": { type: Function, ...{ default: () => null } },'
|
||||||
|
)
|
||||||
|
expect(content).toMatch('"str": {}')
|
||||||
|
expect(content).toMatch('"optional": { required: false }')
|
||||||
|
expect(content).toMatch(
|
||||||
|
'emits: ["update:modelValue", "update:fn", "update:fnWithDefault", "update:str", "update:optional"]'
|
||||||
|
)
|
||||||
|
expect(content).toMatch(
|
||||||
|
`const modelValue = _useModel(__props, "modelValue")`
|
||||||
|
)
|
||||||
|
expect(content).toMatch(`const fn = _useModel(__props, "fn")`)
|
||||||
|
expect(content).toMatch(`const str = _useModel(__props, "str")`)
|
||||||
|
expect(bindings).toStrictEqual({
|
||||||
|
modelValue: BindingTypes.SETUP_REF,
|
||||||
|
fn: BindingTypes.SETUP_REF,
|
||||||
|
fnWithDefault: BindingTypes.SETUP_REF,
|
||||||
|
str: BindingTypes.SETUP_REF,
|
||||||
|
optional: BindingTypes.SETUP_REF
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
test('runtime Enum', () => {
|
test('runtime Enum', () => {
|
||||||
const { content, bindings } = compile(
|
const { content, bindings } = compile(
|
||||||
`<script setup lang="ts">
|
`<script setup lang="ts">
|
||||||
|
|
|
@ -131,17 +131,17 @@ describe('sfc props transform', () => {
|
||||||
`,
|
`,
|
||||||
{ isProd: true }
|
{ isProd: true }
|
||||||
)
|
)
|
||||||
|
assertCode(content)
|
||||||
// literals can be used as-is, non-literals are always returned from a
|
// literals can be used as-is, non-literals are always returned from a
|
||||||
// function
|
// function
|
||||||
expect(content).toMatch(`props: {
|
expect(content).toMatch(`props: {
|
||||||
foo: { default: 1 },
|
foo: { default: 1 },
|
||||||
bar: { default: () => ({}) },
|
bar: { default: () => ({}) },
|
||||||
baz: null,
|
baz: {},
|
||||||
boola: { type: Boolean },
|
boola: { type: Boolean },
|
||||||
boolb: { type: [Boolean, Number] },
|
boolb: { type: [Boolean, Number] },
|
||||||
func: { type: Function, default: () => {} }
|
func: { type: Function, default: () => {} }
|
||||||
}`)
|
}`)
|
||||||
assertCode(content)
|
|
||||||
})
|
})
|
||||||
|
|
||||||
test('aliasing', () => {
|
test('aliasing', () => {
|
||||||
|
|
|
@ -68,6 +68,7 @@ const DEFINE_EXPOSE = 'defineExpose'
|
||||||
const WITH_DEFAULTS = 'withDefaults'
|
const WITH_DEFAULTS = 'withDefaults'
|
||||||
const DEFINE_OPTIONS = 'defineOptions'
|
const DEFINE_OPTIONS = 'defineOptions'
|
||||||
const DEFINE_SLOTS = 'defineSlots'
|
const DEFINE_SLOTS = 'defineSlots'
|
||||||
|
const DEFINE_MODEL = 'defineModel'
|
||||||
|
|
||||||
const isBuiltInDir = makeMap(
|
const isBuiltInDir = makeMap(
|
||||||
`once,memo,if,for,else,else-if,slot,text,html,on,bind,model,show,cloak,is`
|
`once,memo,if,for,else,else-if,slot,text,html,on,bind,model,show,cloak,is`
|
||||||
|
@ -119,13 +120,16 @@ export interface SFCScriptCompileOptions {
|
||||||
* options passed to `compiler-dom`.
|
* options passed to `compiler-dom`.
|
||||||
*/
|
*/
|
||||||
templateOptions?: Partial<SFCTemplateCompileOptions>
|
templateOptions?: Partial<SFCTemplateCompileOptions>
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Hoist <script setup> static constants.
|
* Hoist <script setup> static constants.
|
||||||
* - Only enables when one `<script setup>` exists.
|
* - Only enables when one `<script setup>` exists.
|
||||||
* @default true
|
* @default true
|
||||||
*/
|
*/
|
||||||
hoistStatic?: boolean
|
hoistStatic?: boolean
|
||||||
|
/**
|
||||||
|
* (**Experimental**) Enable macro `defineModel`
|
||||||
|
*/
|
||||||
|
defineModel?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ImportBinding {
|
export interface ImportBinding {
|
||||||
|
@ -150,6 +154,11 @@ type PropsDeclType = FromNormalScript<TSTypeLiteral | TSInterfaceBody>
|
||||||
type EmitsDeclType = FromNormalScript<
|
type EmitsDeclType = FromNormalScript<
|
||||||
TSFunctionType | TSTypeLiteral | TSInterfaceBody
|
TSFunctionType | TSTypeLiteral | TSInterfaceBody
|
||||||
>
|
>
|
||||||
|
interface ModelDecl {
|
||||||
|
type: TSType | undefined
|
||||||
|
options: string | undefined
|
||||||
|
identifier: string | undefined
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Compile `<script setup>`
|
* Compile `<script setup>`
|
||||||
|
@ -164,6 +173,7 @@ export function compileScript(
|
||||||
// feature flags
|
// feature flags
|
||||||
// TODO remove in 3.4
|
// TODO remove in 3.4
|
||||||
const enableReactivityTransform = !!options.reactivityTransform
|
const enableReactivityTransform = !!options.reactivityTransform
|
||||||
|
const enableDefineModel = !!options.defineModel
|
||||||
const isProd = !!options.isProd
|
const isProd = !!options.isProd
|
||||||
const genSourceMap = options.sourceMap !== false
|
const genSourceMap = options.sourceMap !== false
|
||||||
const hoistStatic = options.hoistStatic !== false && !script
|
const hoistStatic = options.hoistStatic !== false && !script
|
||||||
|
@ -314,6 +324,7 @@ export function compileScript(
|
||||||
let hasDefaultExportRender = false
|
let hasDefaultExportRender = false
|
||||||
let hasDefineOptionsCall = false
|
let hasDefineOptionsCall = false
|
||||||
let hasDefineSlotsCall = false
|
let hasDefineSlotsCall = false
|
||||||
|
let hasDefineModelCall = false
|
||||||
let propsRuntimeDecl: Node | undefined
|
let propsRuntimeDecl: Node | undefined
|
||||||
let propsRuntimeDefaults: Node | undefined
|
let propsRuntimeDefaults: Node | undefined
|
||||||
let propsDestructureDecl: Node | undefined
|
let propsDestructureDecl: Node | undefined
|
||||||
|
@ -325,6 +336,7 @@ export function compileScript(
|
||||||
let emitsTypeDecl: EmitsDeclType | undefined
|
let emitsTypeDecl: EmitsDeclType | undefined
|
||||||
let emitIdentifier: string | undefined
|
let emitIdentifier: string | undefined
|
||||||
let optionsRuntimeDecl: Node | undefined
|
let optionsRuntimeDecl: Node | undefined
|
||||||
|
let modelDecls: Record<string, ModelDecl> = {}
|
||||||
let hasAwait = false
|
let hasAwait = false
|
||||||
let hasInlinedSsrRenderFn = false
|
let hasInlinedSsrRenderFn = false
|
||||||
// props/emits declared via types
|
// props/emits declared via types
|
||||||
|
@ -616,6 +628,79 @@ export function compileScript(
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function processDefineModel(node: Node, declId?: LVal): boolean {
|
||||||
|
if (!enableDefineModel || !isCallOf(node, DEFINE_MODEL)) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
hasDefineModelCall = true
|
||||||
|
|
||||||
|
const type =
|
||||||
|
(node.typeParameters && node.typeParameters.params[0]) || undefined
|
||||||
|
let modelName: string
|
||||||
|
let options: Node | undefined
|
||||||
|
const arg0 = node.arguments[0] && unwrapTSNode(node.arguments[0])
|
||||||
|
if (arg0 && arg0.type === 'StringLiteral') {
|
||||||
|
modelName = arg0.value
|
||||||
|
options = node.arguments[1]
|
||||||
|
} else {
|
||||||
|
modelName = 'modelValue'
|
||||||
|
options = arg0
|
||||||
|
}
|
||||||
|
|
||||||
|
if (modelDecls[modelName]) {
|
||||||
|
error(`duplicate model name ${JSON.stringify(modelName)}`, node)
|
||||||
|
}
|
||||||
|
|
||||||
|
const optionsString = options
|
||||||
|
? s.slice(startOffset + options.start!, startOffset + options.end!)
|
||||||
|
: undefined
|
||||||
|
|
||||||
|
modelDecls[modelName] = {
|
||||||
|
type,
|
||||||
|
options: optionsString,
|
||||||
|
identifier:
|
||||||
|
declId && declId.type === 'Identifier' ? declId.name : undefined
|
||||||
|
}
|
||||||
|
|
||||||
|
let runtimeOptions = ''
|
||||||
|
if (options) {
|
||||||
|
if (options.type === 'ObjectExpression') {
|
||||||
|
const local = options.properties.find(
|
||||||
|
p =>
|
||||||
|
p.type === 'ObjectProperty' &&
|
||||||
|
((p.key.type === 'Identifier' && p.key.name === 'local') ||
|
||||||
|
(p.key.type === 'StringLiteral' && p.key.value === 'local'))
|
||||||
|
) as ObjectProperty
|
||||||
|
|
||||||
|
if (local) {
|
||||||
|
runtimeOptions = `{ ${s.slice(
|
||||||
|
startOffset + local.start!,
|
||||||
|
startOffset + local.end!
|
||||||
|
)} }`
|
||||||
|
} else {
|
||||||
|
for (const p of options.properties) {
|
||||||
|
if (p.type === 'SpreadElement' || p.computed) {
|
||||||
|
runtimeOptions = optionsString!
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
runtimeOptions = optionsString!
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
s.overwrite(
|
||||||
|
startOffset + node.start!,
|
||||||
|
startOffset + node.end!,
|
||||||
|
`${helper('useModel')}(__props, ${JSON.stringify(modelName)}${
|
||||||
|
runtimeOptions ? `, ${runtimeOptions}` : ``
|
||||||
|
})`
|
||||||
|
)
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
function getAstBody(): Statement[] {
|
function getAstBody(): Statement[] {
|
||||||
return scriptAst
|
return scriptAst
|
||||||
? [...scriptSetupAst.body, ...scriptAst.body]
|
? [...scriptSetupAst.body, ...scriptAst.body]
|
||||||
|
@ -883,18 +968,25 @@ export function compileScript(
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
function genRuntimeProps(props: Record<string, PropTypeData>) {
|
function concatStrings(strs: Array<string | null | undefined | false>) {
|
||||||
const keys = Object.keys(props)
|
return strs.filter((s): s is string => !!s).join(', ')
|
||||||
if (!keys.length) {
|
}
|
||||||
return ``
|
|
||||||
}
|
function genRuntimeProps() {
|
||||||
const hasStaticDefaults = hasStaticWithDefaults()
|
function genPropsFromTS() {
|
||||||
const scriptSetupSource = scriptSetup!.content
|
const keys = Object.keys(typeDeclaredProps)
|
||||||
let propsDecls = `{
|
if (!keys.length) return
|
||||||
|
|
||||||
|
const hasStaticDefaults = hasStaticWithDefaults()
|
||||||
|
const scriptSetupSource = scriptSetup!.content
|
||||||
|
let propsDecls = `{
|
||||||
${keys
|
${keys
|
||||||
.map(key => {
|
.map(key => {
|
||||||
let defaultString: string | undefined
|
let defaultString: string | undefined
|
||||||
const destructured = genDestructuredDefaultValue(key, props[key].type)
|
const destructured = genDestructuredDefaultValue(
|
||||||
|
key,
|
||||||
|
typeDeclaredProps[key].type
|
||||||
|
)
|
||||||
if (destructured) {
|
if (destructured) {
|
||||||
defaultString = `default: ${destructured.valueString}${
|
defaultString = `default: ${destructured.valueString}${
|
||||||
destructured.needSkipFactory ? `, skipFactory: true` : ``
|
destructured.needSkipFactory ? `, skipFactory: true` : ``
|
||||||
|
@ -924,13 +1016,14 @@ export function compileScript(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const { type, required, skipCheck } = props[key]
|
const { type, required, skipCheck } = typeDeclaredProps[key]
|
||||||
if (!isProd) {
|
if (!isProd) {
|
||||||
return `${key}: { type: ${toRuntimeTypeString(
|
return `${key}: { ${concatStrings([
|
||||||
type
|
`type: ${toRuntimeTypeString(type)}`,
|
||||||
)}, required: ${required}${skipCheck ? ', skipCheck: true' : ''}${
|
`required: ${required}`,
|
||||||
defaultString ? `, ${defaultString}` : ``
|
skipCheck && 'skipCheck: true',
|
||||||
} }`
|
defaultString
|
||||||
|
])} }`
|
||||||
} else if (
|
} else if (
|
||||||
type.some(
|
type.some(
|
||||||
el =>
|
el =>
|
||||||
|
@ -941,24 +1034,104 @@ export function compileScript(
|
||||||
// #4783 for boolean, should keep the type
|
// #4783 for boolean, should keep the type
|
||||||
// #7111 for function, if default value exists or it's not static, should keep it
|
// #7111 for function, if default value exists or it's not static, should keep it
|
||||||
// in production
|
// in production
|
||||||
return `${key}: { type: ${toRuntimeTypeString(type)}${
|
return `${key}: { ${concatStrings([
|
||||||
defaultString ? `, ${defaultString}` : ``
|
`type: ${toRuntimeTypeString(type)}`,
|
||||||
} }`
|
defaultString
|
||||||
|
])} }`
|
||||||
} else {
|
} else {
|
||||||
// production: checks are useless
|
// production: checks are useless
|
||||||
return `${key}: ${defaultString ? `{ ${defaultString} }` : 'null'}`
|
return `${key}: ${defaultString ? `{ ${defaultString} }` : `{}`}`
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.join(',\n ')}\n }`
|
.join(',\n ')}\n }`
|
||||||
|
|
||||||
if (propsRuntimeDefaults && !hasStaticDefaults) {
|
if (propsRuntimeDefaults && !hasStaticDefaults) {
|
||||||
propsDecls = `${helper('mergeDefaults')}(${propsDecls}, ${source.slice(
|
propsDecls = `${helper('mergeDefaults')}(${propsDecls}, ${source.slice(
|
||||||
propsRuntimeDefaults.start! + startOffset,
|
propsRuntimeDefaults.start! + startOffset,
|
||||||
propsRuntimeDefaults.end! + startOffset
|
propsRuntimeDefaults.end! + startOffset
|
||||||
)})`
|
)})`
|
||||||
|
}
|
||||||
|
|
||||||
|
return propsDecls
|
||||||
}
|
}
|
||||||
|
|
||||||
return `\n props: ${propsDecls},`
|
function genModels() {
|
||||||
|
if (!hasDefineModelCall) return
|
||||||
|
|
||||||
|
let modelPropsDecl = ''
|
||||||
|
for (const [name, { type, options }] of Object.entries(modelDecls)) {
|
||||||
|
let skipCheck = false
|
||||||
|
|
||||||
|
let runtimeTypes = type && inferRuntimeType(type, declaredTypes)
|
||||||
|
if (runtimeTypes) {
|
||||||
|
const hasUnknownType = runtimeTypes.includes(UNKNOWN_TYPE)
|
||||||
|
|
||||||
|
runtimeTypes = runtimeTypes.filter(el => {
|
||||||
|
if (el === UNKNOWN_TYPE) return false
|
||||||
|
return isProd
|
||||||
|
? el === 'Boolean' || (el === 'Function' && options)
|
||||||
|
: true
|
||||||
|
})
|
||||||
|
skipCheck = !isProd && hasUnknownType && runtimeTypes.length > 0
|
||||||
|
}
|
||||||
|
|
||||||
|
let runtimeType =
|
||||||
|
(runtimeTypes &&
|
||||||
|
runtimeTypes.length > 0 &&
|
||||||
|
toRuntimeTypeString(runtimeTypes)) ||
|
||||||
|
undefined
|
||||||
|
|
||||||
|
const codegenOptions = concatStrings([
|
||||||
|
runtimeType && `type: ${runtimeType}`,
|
||||||
|
skipCheck && 'skipCheck: true'
|
||||||
|
])
|
||||||
|
|
||||||
|
let decl: string
|
||||||
|
if (runtimeType && options) {
|
||||||
|
decl = isTS
|
||||||
|
? `{ ${codegenOptions}, ...${options} }`
|
||||||
|
: `Object.assign({ ${codegenOptions} }, ${options})`
|
||||||
|
} else {
|
||||||
|
decl = options || (runtimeType ? `{ ${codegenOptions} }` : '{}')
|
||||||
|
}
|
||||||
|
modelPropsDecl += `\n ${JSON.stringify(name)}: ${decl},`
|
||||||
|
}
|
||||||
|
return `{${modelPropsDecl}\n }`
|
||||||
|
}
|
||||||
|
|
||||||
|
let propsDecls: undefined | string
|
||||||
|
if (propsRuntimeDecl) {
|
||||||
|
propsDecls = scriptSetup!.content
|
||||||
|
.slice(propsRuntimeDecl.start!, propsRuntimeDecl.end!)
|
||||||
|
.trim()
|
||||||
|
if (propsDestructureDecl) {
|
||||||
|
const defaults: string[] = []
|
||||||
|
for (const key in propsDestructuredBindings) {
|
||||||
|
const d = genDestructuredDefaultValue(key)
|
||||||
|
if (d)
|
||||||
|
defaults.push(
|
||||||
|
`${key}: ${d.valueString}${
|
||||||
|
d.needSkipFactory ? `, __skip_${key}: true` : ``
|
||||||
|
}`
|
||||||
|
)
|
||||||
|
}
|
||||||
|
if (defaults.length) {
|
||||||
|
propsDecls = `${helper(
|
||||||
|
`mergeDefaults`
|
||||||
|
)}(${propsDecls}, {\n ${defaults.join(',\n ')}\n})`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (propsTypeDecl) {
|
||||||
|
propsDecls = genPropsFromTS()
|
||||||
|
}
|
||||||
|
|
||||||
|
const modelsDecls = genModels()
|
||||||
|
|
||||||
|
if (propsDecls && modelsDecls) {
|
||||||
|
return `${helper('mergeModels')}(${propsDecls}, ${modelsDecls})`
|
||||||
|
} else {
|
||||||
|
return modelsDecls || propsDecls
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function genDestructuredDefaultValue(
|
function genDestructuredDefaultValue(
|
||||||
|
@ -1058,6 +1231,34 @@ export function compileScript(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function genRuntimeEmits() {
|
||||||
|
function genEmitsFromTS() {
|
||||||
|
return typeDeclaredEmits.size
|
||||||
|
? `[${Array.from(typeDeclaredEmits)
|
||||||
|
.map(k => JSON.stringify(k))
|
||||||
|
.join(', ')}]`
|
||||||
|
: ``
|
||||||
|
}
|
||||||
|
|
||||||
|
let emitsDecl = ''
|
||||||
|
if (emitsRuntimeDecl) {
|
||||||
|
emitsDecl = scriptSetup!.content
|
||||||
|
.slice(emitsRuntimeDecl.start!, emitsRuntimeDecl.end!)
|
||||||
|
.trim()
|
||||||
|
} else if (emitsTypeDecl) {
|
||||||
|
emitsDecl = genEmitsFromTS()
|
||||||
|
}
|
||||||
|
if (hasDefineModelCall) {
|
||||||
|
let modelEmitsDecl = `[${Object.keys(modelDecls)
|
||||||
|
.map(n => JSON.stringify(`update:${n}`))
|
||||||
|
.join(', ')}]`
|
||||||
|
emitsDecl = emitsDecl
|
||||||
|
? `${helper('mergeModels')}(${emitsDecl}, ${modelEmitsDecl})`
|
||||||
|
: modelEmitsDecl
|
||||||
|
}
|
||||||
|
return emitsDecl
|
||||||
|
}
|
||||||
|
|
||||||
// 0. parse both <script> and <script setup> blocks
|
// 0. parse both <script> and <script setup> blocks
|
||||||
const scriptAst =
|
const scriptAst =
|
||||||
script &&
|
script &&
|
||||||
|
@ -1345,6 +1546,8 @@ export function compileScript(
|
||||||
callee.end! + startOffset,
|
callee.end! + startOffset,
|
||||||
'__expose'
|
'__expose'
|
||||||
)
|
)
|
||||||
|
} else {
|
||||||
|
processDefineModel(expr)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1370,7 +1573,9 @@ export function compileScript(
|
||||||
processWithDefaults(init, decl.id)
|
processWithDefaults(init, decl.id)
|
||||||
const isDefineEmits =
|
const isDefineEmits =
|
||||||
!isDefineProps && processDefineEmits(init, decl.id)
|
!isDefineProps && processDefineEmits(init, decl.id)
|
||||||
!isDefineEmits && processDefineSlots(init, decl.id)
|
!isDefineEmits &&
|
||||||
|
(processDefineSlots(init, decl.id) ||
|
||||||
|
processDefineModel(init, decl.id))
|
||||||
|
|
||||||
if (isDefineProps || isDefineEmits) {
|
if (isDefineProps || isDefineEmits) {
|
||||||
if (left === 1) {
|
if (left === 1) {
|
||||||
|
@ -1569,6 +1774,9 @@ export function compileScript(
|
||||||
for (const key in typeDeclaredProps) {
|
for (const key in typeDeclaredProps) {
|
||||||
bindingMetadata[key] = BindingTypes.PROPS
|
bindingMetadata[key] = BindingTypes.PROPS
|
||||||
}
|
}
|
||||||
|
for (const key in modelDecls) {
|
||||||
|
bindingMetadata[key] = BindingTypes.PROPS
|
||||||
|
}
|
||||||
// props aliases
|
// props aliases
|
||||||
if (propsDestructureDecl) {
|
if (propsDestructureDecl) {
|
||||||
if (propsDestructureRestId) {
|
if (propsDestructureRestId) {
|
||||||
|
@ -1787,38 +1995,12 @@ export function compileScript(
|
||||||
if (hasInlinedSsrRenderFn) {
|
if (hasInlinedSsrRenderFn) {
|
||||||
runtimeOptions += `\n __ssrInlineRender: true,`
|
runtimeOptions += `\n __ssrInlineRender: true,`
|
||||||
}
|
}
|
||||||
if (propsRuntimeDecl) {
|
|
||||||
let declCode = scriptSetup.content
|
const propsDecl = genRuntimeProps()
|
||||||
.slice(propsRuntimeDecl.start!, propsRuntimeDecl.end!)
|
if (propsDecl) runtimeOptions += `\n props: ${propsDecl},`
|
||||||
.trim()
|
|
||||||
if (propsDestructureDecl) {
|
const emitsDecl = genRuntimeEmits()
|
||||||
const defaults: string[] = []
|
if (emitsDecl) runtimeOptions += `\n emits: ${emitsDecl},`
|
||||||
for (const key in propsDestructuredBindings) {
|
|
||||||
const d = genDestructuredDefaultValue(key)
|
|
||||||
if (d)
|
|
||||||
defaults.push(
|
|
||||||
`${key}: ${d.valueString}${
|
|
||||||
d.needSkipFactory ? `, __skip_${key}: true` : ``
|
|
||||||
}`
|
|
||||||
)
|
|
||||||
}
|
|
||||||
if (defaults.length) {
|
|
||||||
declCode = `${helper(
|
|
||||||
`mergeDefaults`
|
|
||||||
)}(${declCode}, {\n ${defaults.join(',\n ')}\n})`
|
|
||||||
}
|
|
||||||
}
|
|
||||||
runtimeOptions += `\n props: ${declCode},`
|
|
||||||
} else if (propsTypeDecl) {
|
|
||||||
runtimeOptions += genRuntimeProps(typeDeclaredProps)
|
|
||||||
}
|
|
||||||
if (emitsRuntimeDecl) {
|
|
||||||
runtimeOptions += `\n emits: ${scriptSetup.content
|
|
||||||
.slice(emitsRuntimeDecl.start!, emitsRuntimeDecl.end!)
|
|
||||||
.trim()},`
|
|
||||||
} else if (emitsTypeDecl) {
|
|
||||||
runtimeOptions += genRuntimeEmits(typeDeclaredEmits)
|
|
||||||
}
|
|
||||||
|
|
||||||
let definedOptions = ''
|
let definedOptions = ''
|
||||||
if (optionsRuntimeDecl) {
|
if (optionsRuntimeDecl) {
|
||||||
|
@ -1958,7 +2140,10 @@ function walkDeclaration(
|
||||||
? BindingTypes.SETUP_REACTIVE_CONST
|
? BindingTypes.SETUP_REACTIVE_CONST
|
||||||
: BindingTypes.SETUP_CONST
|
: BindingTypes.SETUP_CONST
|
||||||
} else if (isConst) {
|
} else if (isConst) {
|
||||||
if (isCallOf(init, userImportAliases['ref'])) {
|
if (
|
||||||
|
isCallOf(init, userImportAliases['ref']) ||
|
||||||
|
isCallOf(init, DEFINE_MODEL)
|
||||||
|
) {
|
||||||
bindingType = BindingTypes.SETUP_REF
|
bindingType = BindingTypes.SETUP_REF
|
||||||
} else {
|
} else {
|
||||||
bindingType = BindingTypes.SETUP_MAYBE_REF
|
bindingType = BindingTypes.SETUP_MAYBE_REF
|
||||||
|
@ -2393,14 +2578,6 @@ function extractEventNames(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function genRuntimeEmits(emits: Set<string>) {
|
|
||||||
return emits.size
|
|
||||||
? `\n emits: [${Array.from(emits)
|
|
||||||
.map(p => JSON.stringify(p))
|
|
||||||
.join(', ')}],`
|
|
||||||
: ``
|
|
||||||
}
|
|
||||||
|
|
||||||
function canNeverBeRef(node: Node, userReactiveImport?: string): boolean {
|
function canNeverBeRef(node: Node, userReactiveImport?: string): boolean {
|
||||||
if (isCallOf(node, userReactiveImport)) {
|
if (isCallOf(node, userReactiveImport)) {
|
||||||
return true
|
return true
|
||||||
|
|
|
@ -6,9 +6,13 @@ import {
|
||||||
withDefaults,
|
withDefaults,
|
||||||
Slots,
|
Slots,
|
||||||
defineSlots,
|
defineSlots,
|
||||||
VNode
|
VNode,
|
||||||
|
Ref,
|
||||||
|
defineModel
|
||||||
} from 'vue'
|
} from 'vue'
|
||||||
import { describe, expectType } from './utils'
|
import { describe, expectType } from './utils'
|
||||||
|
import { defineComponent } from 'vue'
|
||||||
|
import { useModel } from 'vue'
|
||||||
|
|
||||||
describe('defineProps w/ type declaration', () => {
|
describe('defineProps w/ type declaration', () => {
|
||||||
// type declaration
|
// type declaration
|
||||||
|
@ -202,6 +206,72 @@ describe('defineSlots', () => {
|
||||||
expectType<Slots>(slotsUntype)
|
expectType<Slots>(slotsUntype)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
describe('defineModel', () => {
|
||||||
|
// overload 1
|
||||||
|
const modelValueRequired = defineModel<boolean>({ required: true })
|
||||||
|
expectType<Ref<boolean>>(modelValueRequired)
|
||||||
|
|
||||||
|
// overload 2
|
||||||
|
const modelValue = defineModel<string>()
|
||||||
|
expectType<Ref<string | undefined>>(modelValue)
|
||||||
|
modelValue.value = 'new value'
|
||||||
|
|
||||||
|
const modelValueDefault = defineModel<boolean>({ default: true })
|
||||||
|
expectType<Ref<boolean>>(modelValueDefault)
|
||||||
|
|
||||||
|
// overload 3
|
||||||
|
const countRequired = defineModel<number>('count', { required: false })
|
||||||
|
expectType<Ref<number | undefined>>(countRequired)
|
||||||
|
|
||||||
|
// overload 4
|
||||||
|
const count = defineModel<number>('count')
|
||||||
|
expectType<Ref<number | undefined>>(count)
|
||||||
|
|
||||||
|
const countDefault = defineModel<number>('count', { default: 1 })
|
||||||
|
expectType<Ref<number>>(countDefault)
|
||||||
|
|
||||||
|
// infer type from default
|
||||||
|
const inferred = defineModel({ default: 123 })
|
||||||
|
expectType<Ref<number | undefined>>(inferred)
|
||||||
|
const inferredRequired = defineModel({ default: 123, required: true })
|
||||||
|
expectType<Ref<number>>(inferredRequired)
|
||||||
|
|
||||||
|
// @ts-expect-error type / default mismatch
|
||||||
|
defineModel<string>({ default: 123 })
|
||||||
|
// @ts-expect-error unknown props option
|
||||||
|
defineModel({ foo: 123 })
|
||||||
|
|
||||||
|
// accept defineModel-only options
|
||||||
|
defineModel({ local: true })
|
||||||
|
defineModel('foo', { local: true })
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('useModel', () => {
|
||||||
|
defineComponent({
|
||||||
|
props: ['foo'],
|
||||||
|
setup(props) {
|
||||||
|
const r = useModel(props, 'foo')
|
||||||
|
expectType<Ref<any>>(r)
|
||||||
|
|
||||||
|
// @ts-expect-error
|
||||||
|
useModel(props, 'bar')
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
defineComponent({
|
||||||
|
props: {
|
||||||
|
foo: String,
|
||||||
|
bar: { type: Number, required: true },
|
||||||
|
baz: { type: Boolean }
|
||||||
|
},
|
||||||
|
setup(props) {
|
||||||
|
expectType<Ref<string | undefined>>(useModel(props, 'foo'))
|
||||||
|
expectType<Ref<number>>(useModel(props, 'bar'))
|
||||||
|
expectType<Ref<boolean>>(useModel(props, 'baz'))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
describe('useAttrs', () => {
|
describe('useAttrs', () => {
|
||||||
const attrs = useAttrs()
|
const attrs = useAttrs()
|
||||||
expectType<Record<string, unknown>>(attrs)
|
expectType<Record<string, unknown>>(attrs)
|
||||||
|
|
|
@ -13,7 +13,9 @@ import {
|
||||||
Suspense,
|
Suspense,
|
||||||
computed,
|
computed,
|
||||||
ComputedRef,
|
ComputedRef,
|
||||||
shallowReactive
|
shallowReactive,
|
||||||
|
nextTick,
|
||||||
|
ref
|
||||||
} from '@vue/runtime-test'
|
} from '@vue/runtime-test'
|
||||||
import {
|
import {
|
||||||
defineEmits,
|
defineEmits,
|
||||||
|
@ -24,7 +26,9 @@ import {
|
||||||
useSlots,
|
useSlots,
|
||||||
mergeDefaults,
|
mergeDefaults,
|
||||||
withAsyncContext,
|
withAsyncContext,
|
||||||
createPropsRestProxy
|
createPropsRestProxy,
|
||||||
|
mergeModels,
|
||||||
|
useModel
|
||||||
} from '../src/apiSetupHelpers'
|
} from '../src/apiSetupHelpers'
|
||||||
|
|
||||||
describe('SFC <script setup> helpers', () => {
|
describe('SFC <script setup> helpers', () => {
|
||||||
|
@ -133,6 +137,149 @@ describe('SFC <script setup> helpers', () => {
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
describe('mergeModels', () => {
|
||||||
|
test('array syntax', () => {
|
||||||
|
expect(mergeModels(['foo', 'bar'], ['baz'])).toMatchObject([
|
||||||
|
'foo',
|
||||||
|
'bar',
|
||||||
|
'baz'
|
||||||
|
])
|
||||||
|
})
|
||||||
|
|
||||||
|
test('object syntax', () => {
|
||||||
|
expect(
|
||||||
|
mergeModels({ foo: null, bar: { required: true } }, ['baz'])
|
||||||
|
).toMatchObject({
|
||||||
|
foo: null,
|
||||||
|
bar: { required: true },
|
||||||
|
baz: {}
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(
|
||||||
|
mergeModels(['baz'], { foo: null, bar: { required: true } })
|
||||||
|
).toMatchObject({
|
||||||
|
foo: null,
|
||||||
|
bar: { required: true },
|
||||||
|
baz: {}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
test('overwrite', () => {
|
||||||
|
expect(
|
||||||
|
mergeModels(
|
||||||
|
{ foo: null, bar: { required: true } },
|
||||||
|
{ bar: {}, baz: {} }
|
||||||
|
)
|
||||||
|
).toMatchObject({
|
||||||
|
foo: null,
|
||||||
|
bar: {},
|
||||||
|
baz: {}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('useModel', () => {
|
||||||
|
test('basic', async () => {
|
||||||
|
let foo: any
|
||||||
|
const update = () => {
|
||||||
|
foo.value = 'bar'
|
||||||
|
}
|
||||||
|
|
||||||
|
const Comp = defineComponent({
|
||||||
|
props: ['modelValue'],
|
||||||
|
emits: ['update:modelValue'],
|
||||||
|
setup(props) {
|
||||||
|
foo = useModel(props, 'modelValue')
|
||||||
|
},
|
||||||
|
render() {}
|
||||||
|
})
|
||||||
|
|
||||||
|
const msg = ref('')
|
||||||
|
const setValue = vi.fn(v => (msg.value = v))
|
||||||
|
const root = nodeOps.createElement('div')
|
||||||
|
createApp(() =>
|
||||||
|
h(Comp, {
|
||||||
|
modelValue: msg.value,
|
||||||
|
'onUpdate:modelValue': setValue
|
||||||
|
})
|
||||||
|
).mount(root)
|
||||||
|
|
||||||
|
expect(foo.value).toBe('')
|
||||||
|
expect(msg.value).toBe('')
|
||||||
|
expect(setValue).not.toBeCalled()
|
||||||
|
|
||||||
|
// update from child
|
||||||
|
update()
|
||||||
|
|
||||||
|
await nextTick()
|
||||||
|
expect(msg.value).toBe('bar')
|
||||||
|
expect(foo.value).toBe('bar')
|
||||||
|
expect(setValue).toBeCalledTimes(1)
|
||||||
|
|
||||||
|
// update from parent
|
||||||
|
msg.value = 'qux'
|
||||||
|
|
||||||
|
await nextTick()
|
||||||
|
expect(msg.value).toBe('qux')
|
||||||
|
expect(foo.value).toBe('qux')
|
||||||
|
expect(setValue).toBeCalledTimes(1)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('local', async () => {
|
||||||
|
let foo: any
|
||||||
|
const update = () => {
|
||||||
|
foo.value = 'bar'
|
||||||
|
}
|
||||||
|
|
||||||
|
const Comp = defineComponent({
|
||||||
|
props: ['foo'],
|
||||||
|
emits: ['update:foo'],
|
||||||
|
setup(props) {
|
||||||
|
foo = useModel(props, 'foo', { local: true })
|
||||||
|
},
|
||||||
|
render() {}
|
||||||
|
})
|
||||||
|
|
||||||
|
const root = nodeOps.createElement('div')
|
||||||
|
const updateFoo = vi.fn()
|
||||||
|
render(h(Comp, { 'onUpdate:foo': updateFoo }), root)
|
||||||
|
|
||||||
|
expect(foo.value).toBeUndefined()
|
||||||
|
update()
|
||||||
|
|
||||||
|
expect(foo.value).toBe('bar')
|
||||||
|
|
||||||
|
await nextTick()
|
||||||
|
expect(updateFoo).toBeCalledTimes(1)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('default value', async () => {
|
||||||
|
let count: any
|
||||||
|
const inc = () => {
|
||||||
|
count.value++
|
||||||
|
}
|
||||||
|
const Comp = defineComponent({
|
||||||
|
props: { count: { default: 0 } },
|
||||||
|
emits: ['update:count'],
|
||||||
|
setup(props) {
|
||||||
|
count = useModel(props, 'count', { local: true })
|
||||||
|
},
|
||||||
|
render() {}
|
||||||
|
})
|
||||||
|
|
||||||
|
const root = nodeOps.createElement('div')
|
||||||
|
const updateCount = vi.fn()
|
||||||
|
render(h(Comp, { 'onUpdate:count': updateCount }), root)
|
||||||
|
|
||||||
|
expect(count.value).toBe(0)
|
||||||
|
|
||||||
|
inc()
|
||||||
|
expect(count.value).toBe(1)
|
||||||
|
await nextTick()
|
||||||
|
expect(updateCount).toBeCalledTimes(1)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
test('createPropsRestProxy', () => {
|
test('createPropsRestProxy', () => {
|
||||||
const original = shallowReactive({
|
const original = shallowReactive({
|
||||||
foo: 1,
|
foo: 1,
|
||||||
|
|
|
@ -3,7 +3,8 @@ import {
|
||||||
isPromise,
|
isPromise,
|
||||||
isFunction,
|
isFunction,
|
||||||
Prettify,
|
Prettify,
|
||||||
UnionToIntersection
|
UnionToIntersection,
|
||||||
|
extend
|
||||||
} from '@vue/shared'
|
} from '@vue/shared'
|
||||||
import {
|
import {
|
||||||
getCurrentInstance,
|
getCurrentInstance,
|
||||||
|
@ -12,7 +13,7 @@ import {
|
||||||
createSetupContext,
|
createSetupContext,
|
||||||
unsetCurrentInstance
|
unsetCurrentInstance
|
||||||
} from './component'
|
} from './component'
|
||||||
import { EmitFn, EmitsOptions } from './componentEmits'
|
import { EmitFn, EmitsOptions, ObjectEmitsOptions } from './componentEmits'
|
||||||
import {
|
import {
|
||||||
ComponentOptionsMixin,
|
ComponentOptionsMixin,
|
||||||
ComponentOptionsWithoutProps,
|
ComponentOptionsWithoutProps,
|
||||||
|
@ -22,10 +23,14 @@ import {
|
||||||
import {
|
import {
|
||||||
ComponentPropsOptions,
|
ComponentPropsOptions,
|
||||||
ComponentObjectPropsOptions,
|
ComponentObjectPropsOptions,
|
||||||
ExtractPropTypes
|
ExtractPropTypes,
|
||||||
|
NormalizedProps,
|
||||||
|
PropOptions
|
||||||
} from './componentProps'
|
} from './componentProps'
|
||||||
import { warn } from './warning'
|
import { warn } from './warning'
|
||||||
import { SlotsType, TypedSlots } from './componentSlots'
|
import { SlotsType, TypedSlots } from './componentSlots'
|
||||||
|
import { Ref, ref } from '@vue/reactivity'
|
||||||
|
import { watch } from './apiWatch'
|
||||||
|
|
||||||
// dev only
|
// dev only
|
||||||
const warnRuntimeUsage = (method: string) =>
|
const warnRuntimeUsage = (method: string) =>
|
||||||
|
@ -200,11 +205,77 @@ export function defineOptions<
|
||||||
|
|
||||||
export function defineSlots<
|
export function defineSlots<
|
||||||
S extends Record<string, any> = Record<string, any>
|
S extends Record<string, any> = Record<string, any>
|
||||||
>(): // @ts-expect-error
|
>(): TypedSlots<SlotsType<S>> {
|
||||||
TypedSlots<SlotsType<S>> {
|
|
||||||
if (__DEV__) {
|
if (__DEV__) {
|
||||||
warnRuntimeUsage(`defineSlots`)
|
warnRuntimeUsage(`defineSlots`)
|
||||||
}
|
}
|
||||||
|
return null as any
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* (**Experimental**) Vue `<script setup>` compiler macro for declaring a
|
||||||
|
* two-way binding prop that can be consumed via `v-model` from the parent
|
||||||
|
* component. This will declare a prop with the same name and a corresponding
|
||||||
|
* `update:propName` event.
|
||||||
|
*
|
||||||
|
* If the first argument is a string, it will be used as the prop name;
|
||||||
|
* Otherwise the prop name will default to "modelValue". In both cases, you
|
||||||
|
* can also pass an additional object which will be used as the prop's options.
|
||||||
|
*
|
||||||
|
* The options object can also specify an additional option, `local`. When set
|
||||||
|
* to `true`, the ref can be locally mutated even if the parent did not pass
|
||||||
|
* the matching `v-model`.
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* ```ts
|
||||||
|
* // default model (consumed via `v-model`)
|
||||||
|
* const modelValue = defineModel<string>()
|
||||||
|
* modelValue.value = "hello"
|
||||||
|
*
|
||||||
|
* // default model with options
|
||||||
|
* const modelValue = defineModel<stirng>({ required: true })
|
||||||
|
*
|
||||||
|
* // with specified name (consumed via `v-model:count`)
|
||||||
|
* const count = defineModel<number>('count')
|
||||||
|
* count.value++
|
||||||
|
*
|
||||||
|
* // with specified name and default value
|
||||||
|
* const count = defineModel<number>('count', { default: 0 })
|
||||||
|
*
|
||||||
|
* // local mutable model, can be mutated locally
|
||||||
|
* // even if the parent did not pass the matching `v-model`.
|
||||||
|
* const count = defineModel<number>('count', { local: true, default: 0 })
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
export function defineModel<T>(
|
||||||
|
options: { required: true } & PropOptions<T> & DefineModelOptions
|
||||||
|
): Ref<T>
|
||||||
|
export function defineModel<T>(
|
||||||
|
options: { default: any } & PropOptions<T> & DefineModelOptions
|
||||||
|
): Ref<T>
|
||||||
|
export function defineModel<T>(
|
||||||
|
options?: PropOptions<T> & DefineModelOptions
|
||||||
|
): Ref<T | undefined>
|
||||||
|
export function defineModel<T>(
|
||||||
|
name: string,
|
||||||
|
options: { required: true } & PropOptions<T> & DefineModelOptions
|
||||||
|
): Ref<T>
|
||||||
|
export function defineModel<T>(
|
||||||
|
name: string,
|
||||||
|
options: { default: any } & PropOptions<T> & DefineModelOptions
|
||||||
|
): Ref<T>
|
||||||
|
export function defineModel<T>(
|
||||||
|
name: string,
|
||||||
|
options?: PropOptions<T> & DefineModelOptions
|
||||||
|
): Ref<T | undefined>
|
||||||
|
export function defineModel(): any {
|
||||||
|
if (__DEV__) {
|
||||||
|
warnRuntimeUsage('defineModel')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
interface DefineModelOptions {
|
||||||
|
local?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
type NotUndefined<T> = T extends undefined ? never : T
|
type NotUndefined<T> = T extends undefined ? never : T
|
||||||
|
@ -268,6 +339,55 @@ export function useAttrs(): SetupContext['attrs'] {
|
||||||
return getContext().attrs
|
return getContext().attrs
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function useModel<T extends Record<string, any>, K extends keyof T>(
|
||||||
|
props: T,
|
||||||
|
name: K,
|
||||||
|
options?: { local?: boolean }
|
||||||
|
): Ref<T[K]>
|
||||||
|
export function useModel(
|
||||||
|
props: Record<string, any>,
|
||||||
|
name: string,
|
||||||
|
options?: { local?: boolean }
|
||||||
|
): Ref {
|
||||||
|
const i = getCurrentInstance()!
|
||||||
|
if (__DEV__ && !i) {
|
||||||
|
warn(`useModel() called without active instance.`)
|
||||||
|
return ref() as any
|
||||||
|
}
|
||||||
|
|
||||||
|
if (__DEV__ && !(i.propsOptions[0] as NormalizedProps)[name]) {
|
||||||
|
warn(`useModel() called with prop "${name}" which is not declared.`)
|
||||||
|
return ref() as any
|
||||||
|
}
|
||||||
|
|
||||||
|
if (options && options.local) {
|
||||||
|
const proxy = ref<any>(props[name])
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => props[name],
|
||||||
|
v => (proxy.value = v)
|
||||||
|
)
|
||||||
|
|
||||||
|
watch(proxy, value => {
|
||||||
|
if (value !== props[name]) {
|
||||||
|
i.emit(`update:${name}`, value)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
return proxy
|
||||||
|
} else {
|
||||||
|
return {
|
||||||
|
__v_isRef: true,
|
||||||
|
get value() {
|
||||||
|
return props[name]
|
||||||
|
},
|
||||||
|
set value(value) {
|
||||||
|
i.emit(`update:${name}`, value)
|
||||||
|
}
|
||||||
|
} as any
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function getContext(): SetupContext {
|
function getContext(): SetupContext {
|
||||||
const i = getCurrentInstance()!
|
const i = getCurrentInstance()!
|
||||||
if (__DEV__ && !i) {
|
if (__DEV__ && !i) {
|
||||||
|
@ -276,6 +396,15 @@ function getContext(): SetupContext {
|
||||||
return i.setupContext || (i.setupContext = createSetupContext(i))
|
return i.setupContext || (i.setupContext = createSetupContext(i))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function normalizePropsOrEmits(props: ComponentPropsOptions | EmitsOptions) {
|
||||||
|
return isArray(props)
|
||||||
|
? props.reduce(
|
||||||
|
(normalized, p) => ((normalized[p] = {}), normalized),
|
||||||
|
{} as ComponentObjectPropsOptions | ObjectEmitsOptions
|
||||||
|
)
|
||||||
|
: props
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Runtime helper for merging default declarations. Imported by compiled code
|
* Runtime helper for merging default declarations. Imported by compiled code
|
||||||
* only.
|
* only.
|
||||||
|
@ -285,12 +414,7 @@ export function mergeDefaults(
|
||||||
raw: ComponentPropsOptions,
|
raw: ComponentPropsOptions,
|
||||||
defaults: Record<string, any>
|
defaults: Record<string, any>
|
||||||
): ComponentObjectPropsOptions {
|
): ComponentObjectPropsOptions {
|
||||||
const props = isArray(raw)
|
const props = normalizePropsOrEmits(raw)
|
||||||
? raw.reduce(
|
|
||||||
(normalized, p) => ((normalized[p] = {}), normalized),
|
|
||||||
{} as ComponentObjectPropsOptions
|
|
||||||
)
|
|
||||||
: raw
|
|
||||||
for (const key in defaults) {
|
for (const key in defaults) {
|
||||||
if (key.startsWith('__skip')) continue
|
if (key.startsWith('__skip')) continue
|
||||||
let opt = props[key]
|
let opt = props[key]
|
||||||
|
@ -312,6 +436,20 @@ export function mergeDefaults(
|
||||||
return props
|
return props
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Runtime helper for merging model declarations.
|
||||||
|
* Imported by compiled code only.
|
||||||
|
* @internal
|
||||||
|
*/
|
||||||
|
export function mergeModels(
|
||||||
|
a: ComponentPropsOptions | EmitsOptions,
|
||||||
|
b: ComponentPropsOptions | EmitsOptions
|
||||||
|
) {
|
||||||
|
if (!a || !b) return a || b
|
||||||
|
if (isArray(a) && isArray(b)) return a.concat(b)
|
||||||
|
return extend({}, normalizePropsOrEmits(a), normalizePropsOrEmits(b))
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Used to create a proxy for the rest element when destructuring props with
|
* Used to create a proxy for the rest element when destructuring props with
|
||||||
* defineProps().
|
* defineProps().
|
||||||
|
|
|
@ -627,7 +627,7 @@ function validateProp(
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
// missing but optional
|
// missing but optional
|
||||||
if (value == null && !prop.required) {
|
if (value == null && !required) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
// type check
|
// type check
|
||||||
|
|
|
@ -71,9 +71,12 @@ export {
|
||||||
defineExpose,
|
defineExpose,
|
||||||
defineOptions,
|
defineOptions,
|
||||||
defineSlots,
|
defineSlots,
|
||||||
|
defineModel,
|
||||||
withDefaults,
|
withDefaults,
|
||||||
|
useModel,
|
||||||
// internal
|
// internal
|
||||||
mergeDefaults,
|
mergeDefaults,
|
||||||
|
mergeModels,
|
||||||
createPropsRestProxy,
|
createPropsRestProxy,
|
||||||
withAsyncContext
|
withAsyncContext
|
||||||
} from './apiSetupHelpers'
|
} from './apiSetupHelpers'
|
||||||
|
|
|
@ -5,6 +5,7 @@ type _defineEmits = typeof defineEmits
|
||||||
type _defineExpose = typeof defineExpose
|
type _defineExpose = typeof defineExpose
|
||||||
type _defineOptions = typeof defineOptions
|
type _defineOptions = typeof defineOptions
|
||||||
type _defineSlots = typeof defineSlots
|
type _defineSlots = typeof defineSlots
|
||||||
|
type _defineModel = typeof defineModel
|
||||||
type _withDefaults = typeof withDefaults
|
type _withDefaults = typeof withDefaults
|
||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
|
@ -13,5 +14,6 @@ declare global {
|
||||||
const defineExpose: _defineExpose
|
const defineExpose: _defineExpose
|
||||||
const defineOptions: _defineOptions
|
const defineOptions: _defineOptions
|
||||||
const defineSlots: _defineSlots
|
const defineSlots: _defineSlots
|
||||||
|
const defineModel: _defineModel
|
||||||
const withDefaults: _withDefaults
|
const withDefaults: _withDefaults
|
||||||
}
|
}
|
||||||
|
|
|
@ -37,7 +37,8 @@ const sfcOptions = {
|
||||||
script: {
|
script: {
|
||||||
inlineTemplate: !useDevMode.value,
|
inlineTemplate: !useDevMode.value,
|
||||||
isProd: !useDevMode.value,
|
isProd: !useDevMode.value,
|
||||||
reactivityTransform: true
|
reactivityTransform: true,
|
||||||
|
defineModel: true
|
||||||
},
|
},
|
||||||
style: {
|
style: {
|
||||||
isProd: !useDevMode.value
|
isProd: !useDevMode.value
|
||||||
|
|
Loading…
Reference in New Issue