Skip to content

Commit 467e113

Browse files
authored
feat(compiler-sfc): <script setup> defineProps destructure transform (#4690)
1 parent d84d5ec commit 467e113

File tree

14 files changed

+716
-123
lines changed

14 files changed

+716
-123
lines changed

packages/compiler-core/src/options.ts

+6
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,11 @@ export const enum BindingTypes {
8282
* declared as a prop
8383
*/
8484
PROPS = 'props',
85+
/**
86+
* a local alias of a `<script setup>` destructured prop.
87+
* the original is stored in __propsAliases of the bindingMetadata object.
88+
*/
89+
PROPS_ALIASED = 'props-aliased',
8590
/**
8691
* a let binding (may or may not be a ref)
8792
*/
@@ -110,6 +115,7 @@ export type BindingMetadata = {
110115
[key: string]: BindingTypes | undefined
111116
} & {
112117
__isScriptSetup?: boolean
118+
__propsAliases?: Record<string, string>
113119
}
114120

115121
interface SharedTransformCodegenOptions {

packages/compiler-core/src/transforms/transformExpression.ts

+5
Original file line numberDiff line numberDiff line change
@@ -188,11 +188,16 @@ export function processExpression(
188188
// use __props which is generated by compileScript so in ts mode
189189
// it gets correct type
190190
return `__props.${raw}`
191+
} else if (type === BindingTypes.PROPS_ALIASED) {
192+
// prop with a different local alias (from defineProps() destructure)
193+
return `__props.${bindingMetadata.__propsAliases![raw]}`
191194
}
192195
} else {
193196
if (type && type.startsWith('setup')) {
194197
// setup bindings in non-inline mode
195198
return `$setup.${raw}`
199+
} else if (type === BindingTypes.PROPS_ALIASED) {
200+
return `$props.${bindingMetadata.__propsAliases![raw]}`
196201
} else if (type) {
197202
return `$${type}.${raw}`
198203
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
1+
// Jest Snapshot v1, https://goo.gl/fbAQLP
2+
3+
exports[`sfc props transform aliasing 1`] = `
4+
"import { toDisplayString as _toDisplayString } from \\"vue\\"
5+
6+
7+
export default {
8+
props: ['foo'],
9+
setup(__props) {
10+
11+
12+
let x = foo
13+
let y = __props.foo
14+
15+
return (_ctx, _cache) => {
16+
return _toDisplayString(__props.foo + __props.foo)
17+
}
18+
}
19+
20+
}"
21+
`;
22+
23+
exports[`sfc props transform basic usage 1`] = `
24+
"import { toDisplayString as _toDisplayString } from \\"vue\\"
25+
26+
27+
export default {
28+
props: ['foo'],
29+
setup(__props) {
30+
31+
32+
console.log(__props.foo)
33+
34+
return (_ctx, _cache) => {
35+
return _toDisplayString(__props.foo)
36+
}
37+
}
38+
39+
}"
40+
`;
41+
42+
exports[`sfc props transform default values w/ runtime declaration 1`] = `
43+
"import { mergeDefaults as _mergeDefaults } from 'vue'
44+
45+
export default {
46+
props: _mergeDefaults(['foo', 'bar'], {
47+
foo: 1,
48+
bar: () => {}
49+
}),
50+
setup(__props) {
51+
52+
53+
54+
return () => {}
55+
}
56+
57+
}"
58+
`;
59+
60+
exports[`sfc props transform default values w/ type declaration 1`] = `
61+
"import { defineComponent as _defineComponent } from 'vue'
62+
63+
export default /*#__PURE__*/_defineComponent({
64+
props: {
65+
foo: { type: Number, required: false, default: 1 },
66+
bar: { type: Object, required: false, default: () => {} }
67+
},
68+
setup(__props: any) {
69+
70+
71+
72+
return () => {}
73+
}
74+
75+
})"
76+
`;
77+
78+
exports[`sfc props transform default values w/ type declaration, prod mode 1`] = `
79+
"import { defineComponent as _defineComponent } from 'vue'
80+
81+
export default /*#__PURE__*/_defineComponent({
82+
props: {
83+
foo: { default: 1 },
84+
bar: { default: () => {} },
85+
baz: null
86+
},
87+
setup(__props: any) {
88+
89+
90+
91+
return () => {}
92+
}
93+
94+
})"
95+
`;
96+
97+
exports[`sfc props transform nested scope 1`] = `
98+
"export default {
99+
props: ['foo', 'bar'],
100+
setup(__props) {
101+
102+
103+
function test(foo) {
104+
console.log(foo)
105+
console.log(__props.bar)
106+
}
107+
108+
return () => {}
109+
}
110+
111+
}"
112+
`;
113+
114+
exports[`sfc props transform rest spread 1`] = `
115+
"import { createPropsRestProxy as _createPropsRestProxy } from 'vue'
116+
117+
export default {
118+
props: ['foo', 'bar', 'baz'],
119+
setup(__props) {
120+
121+
const rest = _createPropsRestProxy(__props, [\\"foo\\",\\"bar\\"])
122+
123+
124+
return () => {}
125+
}
126+
127+
}"
128+
`;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,191 @@
1+
import { BindingTypes } from '@vue/compiler-core'
2+
import { SFCScriptCompileOptions } from '../src'
3+
import { compileSFCScript, assertCode } from './utils'
4+
5+
describe('sfc props transform', () => {
6+
function compile(src: string, options?: Partial<SFCScriptCompileOptions>) {
7+
return compileSFCScript(src, {
8+
inlineTemplate: true,
9+
propsDestructureTransform: true,
10+
...options
11+
})
12+
}
13+
14+
test('basic usage', () => {
15+
const { content, bindings } = compile(`
16+
<script setup>
17+
const { foo } = defineProps(['foo'])
18+
console.log(foo)
19+
</script>
20+
<template>{{ foo }}</template>
21+
`)
22+
expect(content).not.toMatch(`const { foo } =`)
23+
expect(content).toMatch(`console.log(__props.foo)`)
24+
expect(content).toMatch(`_toDisplayString(__props.foo)`)
25+
assertCode(content)
26+
expect(bindings).toStrictEqual({
27+
foo: BindingTypes.PROPS
28+
})
29+
})
30+
31+
test('nested scope', () => {
32+
const { content, bindings } = compile(`
33+
<script setup>
34+
const { foo, bar } = defineProps(['foo', 'bar'])
35+
function test(foo) {
36+
console.log(foo)
37+
console.log(bar)
38+
}
39+
</script>
40+
`)
41+
expect(content).not.toMatch(`const { foo, bar } =`)
42+
expect(content).toMatch(`console.log(foo)`)
43+
expect(content).toMatch(`console.log(__props.bar)`)
44+
assertCode(content)
45+
expect(bindings).toStrictEqual({
46+
foo: BindingTypes.PROPS,
47+
bar: BindingTypes.PROPS,
48+
test: BindingTypes.SETUP_CONST
49+
})
50+
})
51+
52+
test('default values w/ runtime declaration', () => {
53+
const { content } = compile(`
54+
<script setup>
55+
const { foo = 1, bar = {} } = defineProps(['foo', 'bar'])
56+
</script>
57+
`)
58+
// literals can be used as-is, non-literals are always returned from a
59+
// function
60+
expect(content).toMatch(`props: _mergeDefaults(['foo', 'bar'], {
61+
foo: 1,
62+
bar: () => {}
63+
})`)
64+
assertCode(content)
65+
})
66+
67+
test('default values w/ type declaration', () => {
68+
const { content } = compile(`
69+
<script setup lang="ts">
70+
const { foo = 1, bar = {} } = defineProps<{ foo?: number, bar?: object }>()
71+
</script>
72+
`)
73+
// literals can be used as-is, non-literals are always returned from a
74+
// function
75+
expect(content).toMatch(`props: {
76+
foo: { type: Number, required: false, default: 1 },
77+
bar: { type: Object, required: false, default: () => {} }
78+
}`)
79+
assertCode(content)
80+
})
81+
82+
test('default values w/ type declaration, prod mode', () => {
83+
const { content } = compile(
84+
`
85+
<script setup lang="ts">
86+
const { foo = 1, bar = {} } = defineProps<{ foo?: number, bar?: object, baz?: any }>()
87+
</script>
88+
`,
89+
{ isProd: true }
90+
)
91+
// literals can be used as-is, non-literals are always returned from a
92+
// function
93+
expect(content).toMatch(`props: {
94+
foo: { default: 1 },
95+
bar: { default: () => {} },
96+
baz: null
97+
}`)
98+
assertCode(content)
99+
})
100+
101+
test('aliasing', () => {
102+
const { content, bindings } = compile(`
103+
<script setup>
104+
const { foo: bar } = defineProps(['foo'])
105+
let x = foo
106+
let y = bar
107+
</script>
108+
<template>{{ foo + bar }}</template>
109+
`)
110+
expect(content).not.toMatch(`const { foo: bar } =`)
111+
expect(content).toMatch(`let x = foo`) // should not process
112+
expect(content).toMatch(`let y = __props.foo`)
113+
// should convert bar to __props.foo in template expressions
114+
expect(content).toMatch(`_toDisplayString(__props.foo + __props.foo)`)
115+
assertCode(content)
116+
expect(bindings).toStrictEqual({
117+
x: BindingTypes.SETUP_LET,
118+
y: BindingTypes.SETUP_LET,
119+
foo: BindingTypes.PROPS,
120+
bar: BindingTypes.PROPS_ALIASED,
121+
__propsAliases: {
122+
bar: 'foo'
123+
}
124+
})
125+
})
126+
127+
test('rest spread', () => {
128+
const { content, bindings } = compile(`
129+
<script setup>
130+
const { foo, bar, ...rest } = defineProps(['foo', 'bar', 'baz'])
131+
</script>
132+
`)
133+
expect(content).toMatch(
134+
`const rest = _createPropsRestProxy(__props, ["foo","bar"])`
135+
)
136+
assertCode(content)
137+
expect(bindings).toStrictEqual({
138+
foo: BindingTypes.PROPS,
139+
bar: BindingTypes.PROPS,
140+
baz: BindingTypes.PROPS,
141+
rest: BindingTypes.SETUP_CONST
142+
})
143+
})
144+
145+
describe('errors', () => {
146+
test('should error on deep destructure', () => {
147+
expect(() =>
148+
compile(
149+
`<script setup>const { foo: [bar] } = defineProps(['foo'])</script>`
150+
)
151+
).toThrow(`destructure does not support nested patterns`)
152+
153+
expect(() =>
154+
compile(
155+
`<script setup>const { foo: { bar } } = defineProps(['foo'])</script>`
156+
)
157+
).toThrow(`destructure does not support nested patterns`)
158+
})
159+
160+
test('should error on computed key', () => {
161+
expect(() =>
162+
compile(
163+
`<script setup>const { [foo]: bar } = defineProps(['foo'])</script>`
164+
)
165+
).toThrow(`destructure cannot use computed key`)
166+
})
167+
168+
test('should error when used with withDefaults', () => {
169+
expect(() =>
170+
compile(
171+
`<script setup lang="ts">
172+
const { foo } = withDefaults(defineProps<{ foo: string }>(), { foo: 'foo' })
173+
</script>`
174+
)
175+
).toThrow(`withDefaults() is unnecessary when using destructure`)
176+
})
177+
178+
test('should error if destructure reference local vars', () => {
179+
expect(() =>
180+
compile(
181+
`<script setup>
182+
const x = 1
183+
const {
184+
foo = () => x
185+
} = defineProps(['foo'])
186+
</script>`
187+
)
188+
).toThrow(`cannot reference locally declared variables`)
189+
})
190+
})
191+
})

0 commit comments

Comments
 (0)