compileScriptPropsDestructure.spec.ts 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358
  1. import { BindingTypes } from '@vue/compiler-core'
  2. import { SFCScriptCompileOptions } from '../src'
  3. import { compileSFCScript, assertCode } from './utils'
  4. describe('sfc props transform', () => {
  5. function compile(src: string, options?: Partial<SFCScriptCompileOptions>) {
  6. return compileSFCScript(src, {
  7. inlineTemplate: true,
  8. ...options
  9. })
  10. }
  11. test('basic usage', () => {
  12. const { content, bindings } = compile(`
  13. <script setup>
  14. const { foo } = defineProps(['foo'])
  15. console.log(foo)
  16. </script>
  17. <template>{{ foo }}</template>
  18. `)
  19. expect(content).not.toMatch(`const { foo } =`)
  20. expect(content).toMatch(`console.log(__props.foo)`)
  21. expect(content).toMatch(`_toDisplayString(__props.foo)`)
  22. assertCode(content)
  23. expect(bindings).toStrictEqual({
  24. foo: BindingTypes.PROPS
  25. })
  26. })
  27. test('multiple variable declarations', () => {
  28. const { content, bindings } = compile(`
  29. <script setup>
  30. const bar = 'fish', { foo } = defineProps(['foo']), hello = 'world'
  31. </script>
  32. <template><div>{{ foo }} {{ hello }} {{ bar }}</div></template>
  33. `)
  34. expect(content).not.toMatch(`const { foo } =`)
  35. expect(content).toMatch(`const bar = 'fish', hello = 'world'`)
  36. expect(content).toMatch(`_toDisplayString(hello)`)
  37. expect(content).toMatch(`_toDisplayString(bar)`)
  38. expect(content).toMatch(`_toDisplayString(__props.foo)`)
  39. assertCode(content)
  40. expect(bindings).toStrictEqual({
  41. foo: BindingTypes.PROPS,
  42. bar: BindingTypes.LITERAL_CONST,
  43. hello: BindingTypes.LITERAL_CONST
  44. })
  45. })
  46. test('nested scope', () => {
  47. const { content, bindings } = compile(`
  48. <script setup>
  49. const { foo, bar } = defineProps(['foo', 'bar'])
  50. function test(foo) {
  51. console.log(foo)
  52. console.log(bar)
  53. }
  54. </script>
  55. `)
  56. expect(content).not.toMatch(`const { foo, bar } =`)
  57. expect(content).toMatch(`console.log(foo)`)
  58. expect(content).toMatch(`console.log(__props.bar)`)
  59. assertCode(content)
  60. expect(bindings).toStrictEqual({
  61. foo: BindingTypes.PROPS,
  62. bar: BindingTypes.PROPS,
  63. test: BindingTypes.SETUP_CONST
  64. })
  65. })
  66. test('default values w/ array runtime declaration', () => {
  67. const { content } = compile(`
  68. <script setup>
  69. const { foo = 1, bar = {}, func = () => {} } = defineProps(['foo', 'bar', 'baz'])
  70. </script>
  71. `)
  72. // literals can be used as-is, non-literals are always returned from a
  73. // function
  74. // functions need to be marked with a skip marker
  75. expect(content).toMatch(`props: _mergeDefaults(['foo', 'bar', 'baz'], {
  76. foo: 1,
  77. bar: () => ({}),
  78. func: () => {}, __skip_func: true
  79. })`)
  80. assertCode(content)
  81. })
  82. test('default values w/ object runtime declaration', () => {
  83. const { content } = compile(`
  84. <script setup>
  85. const { foo = 1, bar = {}, func = () => {}, ext = x } = defineProps({ foo: Number, bar: Object, func: Function, ext: null })
  86. </script>
  87. `)
  88. // literals can be used as-is, non-literals are always returned from a
  89. // function
  90. // functions need to be marked with a skip marker since we cannot always
  91. // safely infer whether runtime type is Function (e.g. if the runtime decl
  92. // is imported, or spreads another object)
  93. expect(content)
  94. .toMatch(`props: _mergeDefaults({ foo: Number, bar: Object, func: Function, ext: null }, {
  95. foo: 1,
  96. bar: () => ({}),
  97. func: () => {}, __skip_func: true,
  98. ext: x, __skip_ext: true
  99. })`)
  100. assertCode(content)
  101. })
  102. test('default values w/ type declaration', () => {
  103. const { content } = compile(`
  104. <script setup lang="ts">
  105. const { foo = 1, bar = {}, func = () => {} } = defineProps<{ foo?: number, bar?: object, func?: () => any }>()
  106. </script>
  107. `)
  108. // literals can be used as-is, non-literals are always returned from a
  109. // function
  110. expect(content).toMatch(`props: {
  111. foo: { type: Number, required: false, default: 1 },
  112. bar: { type: Object, required: false, default: () => ({}) },
  113. func: { type: Function, required: false, default: () => {} }
  114. }`)
  115. assertCode(content)
  116. })
  117. test('default values w/ type declaration, prod mode', () => {
  118. const { content } = compile(
  119. `
  120. <script setup lang="ts">
  121. const { foo = 1, bar = {}, func = () => {} } = defineProps<{ foo?: number, bar?: object, baz?: any, boola?: boolean, boolb?: boolean | number, func?: Function }>()
  122. </script>
  123. `,
  124. { isProd: true }
  125. )
  126. // literals can be used as-is, non-literals are always returned from a
  127. // function
  128. expect(content).toMatch(`props: {
  129. foo: { default: 1 },
  130. bar: { default: () => ({}) },
  131. baz: null,
  132. boola: { type: Boolean },
  133. boolb: { type: [Boolean, Number] },
  134. func: { type: Function, default: () => {} }
  135. }`)
  136. assertCode(content)
  137. })
  138. test('aliasing', () => {
  139. const { content, bindings } = compile(`
  140. <script setup>
  141. const { foo: bar } = defineProps(['foo'])
  142. let x = foo
  143. let y = bar
  144. </script>
  145. <template>{{ foo + bar }}</template>
  146. `)
  147. expect(content).not.toMatch(`const { foo: bar } =`)
  148. expect(content).toMatch(`let x = foo`) // should not process
  149. expect(content).toMatch(`let y = __props.foo`)
  150. // should convert bar to __props.foo in template expressions
  151. expect(content).toMatch(`_toDisplayString(__props.foo + __props.foo)`)
  152. assertCode(content)
  153. expect(bindings).toStrictEqual({
  154. x: BindingTypes.SETUP_LET,
  155. y: BindingTypes.SETUP_LET,
  156. foo: BindingTypes.PROPS,
  157. bar: BindingTypes.PROPS_ALIASED,
  158. __propsAliases: {
  159. bar: 'foo'
  160. }
  161. })
  162. })
  163. // #5425
  164. test('non-identifier prop names', () => {
  165. const { content, bindings } = compile(`
  166. <script setup>
  167. const { 'foo.bar': fooBar } = defineProps({ 'foo.bar': Function })
  168. let x = fooBar
  169. </script>
  170. <template>{{ fooBar }}</template>
  171. `)
  172. expect(content).toMatch(`x = __props["foo.bar"]`)
  173. expect(content).toMatch(`toDisplayString(__props["foo.bar"])`)
  174. assertCode(content)
  175. expect(bindings).toStrictEqual({
  176. x: BindingTypes.SETUP_LET,
  177. 'foo.bar': BindingTypes.PROPS,
  178. fooBar: BindingTypes.PROPS_ALIASED,
  179. __propsAliases: {
  180. fooBar: 'foo.bar'
  181. }
  182. })
  183. })
  184. test('rest spread', () => {
  185. const { content, bindings } = compile(`
  186. <script setup>
  187. const { foo, bar, ...rest } = defineProps(['foo', 'bar', 'baz'])
  188. </script>
  189. `)
  190. expect(content).toMatch(
  191. `const rest = _createPropsRestProxy(__props, ["foo","bar"])`
  192. )
  193. assertCode(content)
  194. expect(bindings).toStrictEqual({
  195. foo: BindingTypes.PROPS,
  196. bar: BindingTypes.PROPS,
  197. baz: BindingTypes.PROPS,
  198. rest: BindingTypes.SETUP_REACTIVE_CONST
  199. })
  200. })
  201. // #6960
  202. test('computed static key', () => {
  203. const { content, bindings } = compile(`
  204. <script setup>
  205. const { ['foo']: foo } = defineProps(['foo'])
  206. console.log(foo)
  207. </script>
  208. <template>{{ foo }}</template>
  209. `)
  210. expect(content).not.toMatch(`const { foo } =`)
  211. expect(content).toMatch(`console.log(__props.foo)`)
  212. expect(content).toMatch(`_toDisplayString(__props.foo)`)
  213. assertCode(content)
  214. expect(bindings).toStrictEqual({
  215. foo: BindingTypes.PROPS
  216. })
  217. })
  218. describe('errors', () => {
  219. test('should error on deep destructure', () => {
  220. expect(() =>
  221. compile(
  222. `<script setup>const { foo: [bar] } = defineProps(['foo'])</script>`
  223. )
  224. ).toThrow(`destructure does not support nested patterns`)
  225. expect(() =>
  226. compile(
  227. `<script setup>const { foo: { bar } } = defineProps(['foo'])</script>`
  228. )
  229. ).toThrow(`destructure does not support nested patterns`)
  230. })
  231. test('should error on computed key', () => {
  232. expect(() =>
  233. compile(
  234. `<script setup>const { [foo]: bar } = defineProps(['foo'])</script>`
  235. )
  236. ).toThrow(`destructure cannot use computed key`)
  237. })
  238. test('should error when used with withDefaults', () => {
  239. expect(() =>
  240. compile(
  241. `<script setup lang="ts">
  242. const { foo } = withDefaults(defineProps<{ foo: string }>(), { foo: 'foo' })
  243. </script>`
  244. )
  245. ).toThrow(`withDefaults() is unnecessary when using destructure`)
  246. })
  247. test('should error if destructure reference local vars', () => {
  248. expect(() =>
  249. compile(
  250. `<script setup>
  251. let x = 1
  252. const {
  253. foo = () => x
  254. } = defineProps(['foo'])
  255. </script>`
  256. )
  257. ).toThrow(`cannot reference locally declared variables`)
  258. })
  259. test('should error if assignment to destructured prop binding', () => {
  260. expect(() =>
  261. compile(
  262. `<script setup>
  263. const { foo } = defineProps(['foo'])
  264. foo = 'bar'
  265. </script>`
  266. )
  267. ).toThrow(`Cannot assign to destructured props`)
  268. expect(() =>
  269. compile(
  270. `<script setup>
  271. let { foo } = defineProps(['foo'])
  272. foo = 'bar'
  273. </script>`
  274. )
  275. ).toThrow(`Cannot assign to destructured props`)
  276. })
  277. test('should error when passing destructured prop into certain methods', () => {
  278. expect(() =>
  279. compile(
  280. `<script setup>
  281. import { watch } from 'vue'
  282. const { foo } = defineProps(['foo'])
  283. watch(foo, () => {})
  284. </script>`
  285. )
  286. ).toThrow(
  287. `"foo" is a destructured prop and should not be passed directly to watch().`
  288. )
  289. expect(() =>
  290. compile(
  291. `<script setup>
  292. import { watch as w } from 'vue'
  293. const { foo } = defineProps(['foo'])
  294. w(foo, () => {})
  295. </script>`
  296. )
  297. ).toThrow(
  298. `"foo" is a destructured prop and should not be passed directly to watch().`
  299. )
  300. expect(() =>
  301. compile(
  302. `<script setup>
  303. import { toRef } from 'vue'
  304. const { foo } = defineProps(['foo'])
  305. toRef(foo)
  306. </script>`
  307. )
  308. ).toThrow(
  309. `"foo" is a destructured prop and should not be passed directly to toRef().`
  310. )
  311. expect(() =>
  312. compile(
  313. `<script setup>
  314. import { toRef as r } from 'vue'
  315. const { foo } = defineProps(['foo'])
  316. r(foo)
  317. </script>`
  318. )
  319. ).toThrow(
  320. `"foo" is a destructured prop and should not be passed directly to toRef().`
  321. )
  322. })
  323. // not comprehensive, but should help for most common cases
  324. test('should error if default value type does not match declared type', () => {
  325. expect(() =>
  326. compile(
  327. `<script setup lang="ts">
  328. const { foo = 'hello' } = defineProps<{ foo?: number }>()
  329. </script>`
  330. )
  331. ).toThrow(`Default value of prop "foo" does not match declared type.`)
  332. })
  333. })
  334. })