definePropsDestructure.spec.ts 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574
  1. import { BindingTypes } from '@vue/compiler-core'
  2. import type { SFCScriptCompileOptions } from '../../src'
  3. import { assertCode, compileSFCScript } from '../utils'
  4. describe('sfc reactive props destructure', () => {
  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('for-of loop variable shadowing', () => {
  67. const { content } = compile(`
  68. <script setup lang="ts">
  69. interface Props {
  70. msg: string;
  71. input: string[];
  72. }
  73. const { msg, input } = defineProps<Props>();
  74. for (const msg of input) {
  75. console.log('MESSAGE', msg);
  76. }
  77. console.log('NOT FAIL', { msg });
  78. </script>
  79. `)
  80. // inside loop: should use local variable
  81. expect(content).toMatch(`for (const msg of __props.input)`)
  82. expect(content).toMatch(`console.log('MESSAGE', msg)`)
  83. // after loop: should restore to prop reference
  84. expect(content).toMatch(`console.log('NOT FAIL', { msg: __props.msg })`)
  85. assertCode(content)
  86. })
  87. test('regular for loop variable shadowing', () => {
  88. const { content } = compile(`
  89. <script setup lang="ts">
  90. const { i, len } = defineProps<{ i: number; len: number }>();
  91. for (let i = 0; i < len; i++) {
  92. console.log('INDEX', i);
  93. }
  94. console.log('AFTER', { i });
  95. </script>
  96. `)
  97. // inside loop: should use local variable
  98. expect(content).toMatch(`for (let i = 0; i < __props.len; i++)`)
  99. expect(content).toMatch(`console.log('INDEX', i)`)
  100. // after loop: should restore to prop reference
  101. expect(content).toMatch(`console.log('AFTER', { i: __props.i })`)
  102. assertCode(content)
  103. })
  104. test('default values w/ array runtime declaration', () => {
  105. const { content } = compile(`
  106. <script setup>
  107. const { foo = 1, bar = {}, func = () => {} } = defineProps(['foo', 'bar', 'baz'])
  108. </script>
  109. `)
  110. // literals can be used as-is, non-literals are always returned from a
  111. // function
  112. // functions need to be marked with a skip marker
  113. expect(content)
  114. .toMatch(`props: /*@__PURE__*/_mergeDefaults(['foo', 'bar', 'baz'], {
  115. foo: 1,
  116. bar: () => ({}),
  117. func: () => {}, __skip_func: true
  118. })`)
  119. assertCode(content)
  120. })
  121. test('default values w/ object runtime declaration', () => {
  122. const { content } = compile(`
  123. <script setup>
  124. const { foo = 1, bar = {}, func = () => {}, ext = x } = defineProps({ foo: Number, bar: Object, func: Function, ext: null })
  125. </script>
  126. `)
  127. // literals can be used as-is, non-literals are always returned from a
  128. // function
  129. // functions need to be marked with a skip marker since we cannot always
  130. // safely infer whether runtime type is Function (e.g. if the runtime decl
  131. // is imported, or spreads another object)
  132. expect(content)
  133. .toMatch(`props: /*@__PURE__*/_mergeDefaults({ foo: Number, bar: Object, func: Function, ext: null }, {
  134. foo: 1,
  135. bar: () => ({}),
  136. func: () => {}, __skip_func: true,
  137. ext: x, __skip_ext: true
  138. })`)
  139. assertCode(content)
  140. })
  141. test('default values w/ runtime declaration & key is string', () => {
  142. const { content, bindings } = compile(`
  143. <script setup>
  144. const { foo = 1, 'foo:bar': fooBar = 'foo-bar' } = defineProps(['foo', 'foo:bar'])
  145. </script>
  146. `)
  147. expect(bindings).toStrictEqual({
  148. __propsAliases: {
  149. fooBar: 'foo:bar',
  150. },
  151. foo: BindingTypes.PROPS,
  152. 'foo:bar': BindingTypes.PROPS,
  153. fooBar: BindingTypes.PROPS_ALIASED,
  154. })
  155. expect(content).toMatch(`
  156. props: /*@__PURE__*/_mergeDefaults(['foo', 'foo:bar'], {
  157. foo: 1,
  158. "foo:bar": 'foo-bar'
  159. }),`)
  160. assertCode(content)
  161. })
  162. test('default values w/ type declaration', () => {
  163. const { content } = compile(`
  164. <script setup lang="ts">
  165. const { foo = 1, bar = {}, func = () => {} } = defineProps<{ foo?: number, bar?: object, func?: () => any }>()
  166. </script>
  167. `)
  168. // literals can be used as-is, non-literals are always returned from a
  169. // function
  170. expect(content).toMatch(`props: {
  171. foo: { type: Number, required: false, default: 1 },
  172. bar: { type: Object, required: false, default: () => ({}) },
  173. func: { type: Function, required: false, default: () => {} }
  174. }`)
  175. assertCode(content)
  176. })
  177. test('default values w/ type declaration & key is string', () => {
  178. const { content, bindings } = compile(`
  179. <script setup lang="ts">
  180. const { foo = 1, bar = 2, 'foo:bar': fooBar = 'foo-bar' } = defineProps<{
  181. "foo": number // double-quoted string
  182. 'bar': number // single-quoted string
  183. 'foo:bar': string // single-quoted string containing symbols
  184. "onUpdate:modelValue": (val: number) => void // double-quoted string containing symbols
  185. }>()
  186. </script>
  187. <template>{{ foo }}</template>
  188. `)
  189. expect(bindings).toStrictEqual({
  190. __propsAliases: {
  191. fooBar: 'foo:bar',
  192. },
  193. foo: BindingTypes.PROPS,
  194. bar: BindingTypes.PROPS,
  195. 'foo:bar': BindingTypes.PROPS,
  196. fooBar: BindingTypes.PROPS_ALIASED,
  197. 'onUpdate:modelValue': BindingTypes.PROPS,
  198. })
  199. expect(content).toMatch(`
  200. props: {
  201. foo: { type: Number, required: true, default: 1 },
  202. bar: { type: Number, required: true, default: 2 },
  203. "foo:bar": { type: String, required: true, default: 'foo-bar' },
  204. "onUpdate:modelValue": { type: Function, required: true }
  205. },`)
  206. expect(content).toMatch(`__props.foo`)
  207. assertCode(content)
  208. })
  209. test('default values w/ type declaration, prod mode', () => {
  210. const { content } = compile(
  211. `
  212. <script setup lang="ts">
  213. const { foo = 1, bar = {}, func = () => {} } = defineProps<{ foo?: number, bar?: object, baz?: any, boola?: boolean, boolb?: boolean | number, func?: Function }>()
  214. </script>
  215. `,
  216. { isProd: true },
  217. )
  218. assertCode(content)
  219. // literals can be used as-is, non-literals are always returned from a
  220. // function
  221. expect(content).toMatch(`props: {
  222. foo: { default: 1 },
  223. bar: { default: () => ({}) },
  224. baz: {},
  225. boola: { type: Boolean },
  226. boolb: { type: [Boolean, Number] },
  227. func: { type: Function, default: () => {} }
  228. }`)
  229. })
  230. test('with TSInstantiationExpression', () => {
  231. const { content } = compile(
  232. `
  233. <script setup lang="ts">
  234. type Foo = <T extends string | number>(data: T) => void
  235. const { value } = defineProps<{ value: Foo }>()
  236. const foo = value<123>
  237. </script>
  238. `,
  239. { isProd: true },
  240. )
  241. assertCode(content)
  242. expect(content).toMatch(`const foo = __props.value<123>`)
  243. })
  244. test('aliasing', () => {
  245. const { content, bindings } = compile(`
  246. <script setup>
  247. const { foo: bar } = defineProps(['foo'])
  248. let x = foo
  249. let y = bar
  250. </script>
  251. <template>{{ foo + bar }}</template>
  252. `)
  253. expect(content).not.toMatch(`const { foo: bar } =`)
  254. expect(content).toMatch(`let x = foo`) // should not process
  255. expect(content).toMatch(`let y = __props.foo`)
  256. // should convert bar to __props.foo in template expressions
  257. expect(content).toMatch(`_toDisplayString(__props.foo + __props.foo)`)
  258. assertCode(content)
  259. expect(bindings).toStrictEqual({
  260. x: BindingTypes.SETUP_LET,
  261. y: BindingTypes.SETUP_LET,
  262. foo: BindingTypes.PROPS,
  263. bar: BindingTypes.PROPS_ALIASED,
  264. __propsAliases: {
  265. bar: 'foo',
  266. },
  267. })
  268. })
  269. // #5425
  270. test('non-identifier prop names', () => {
  271. const { content, bindings } = compile(`
  272. <script setup>
  273. const { 'foo.bar': fooBar } = defineProps({ 'foo.bar': Function })
  274. let x = fooBar
  275. </script>
  276. <template>{{ fooBar }}</template>
  277. `)
  278. expect(content).toMatch(`x = __props["foo.bar"]`)
  279. expect(content).toMatch(`toDisplayString(__props["foo.bar"])`)
  280. assertCode(content)
  281. expect(bindings).toStrictEqual({
  282. x: BindingTypes.SETUP_LET,
  283. 'foo.bar': BindingTypes.PROPS,
  284. fooBar: BindingTypes.PROPS_ALIASED,
  285. __propsAliases: {
  286. fooBar: 'foo.bar',
  287. },
  288. })
  289. })
  290. test('rest spread', () => {
  291. const { content, bindings } = compile(`
  292. <script setup>
  293. const { foo, bar, ...rest } = defineProps(['foo', 'bar', 'baz'])
  294. </script>
  295. `)
  296. expect(content).toMatch(
  297. `const rest = _createPropsRestProxy(__props, ["foo","bar"])`,
  298. )
  299. assertCode(content)
  300. expect(bindings).toStrictEqual({
  301. foo: BindingTypes.PROPS,
  302. bar: BindingTypes.PROPS,
  303. baz: BindingTypes.PROPS,
  304. rest: BindingTypes.SETUP_REACTIVE_CONST,
  305. })
  306. })
  307. test('rest spread non-inline', () => {
  308. const { content, bindings } = compile(
  309. `
  310. <script setup>
  311. const { foo, ...rest } = defineProps(['foo', 'bar'])
  312. </script>
  313. <template>{{ rest.bar }}</template>
  314. `,
  315. { inlineTemplate: false },
  316. )
  317. expect(content).toMatch(
  318. `const rest = _createPropsRestProxy(__props, ["foo"])`,
  319. )
  320. assertCode(content)
  321. expect(bindings).toStrictEqual({
  322. foo: BindingTypes.PROPS,
  323. bar: BindingTypes.PROPS,
  324. rest: BindingTypes.SETUP_REACTIVE_CONST,
  325. })
  326. })
  327. // #6960
  328. test('computed static key', () => {
  329. const { content, bindings } = compile(`
  330. <script setup>
  331. const { ['foo']: foo } = defineProps(['foo'])
  332. console.log(foo)
  333. </script>
  334. <template>{{ foo }}</template>
  335. `)
  336. expect(content).not.toMatch(`const { foo } =`)
  337. expect(content).toMatch(`console.log(__props.foo)`)
  338. expect(content).toMatch(`_toDisplayString(__props.foo)`)
  339. assertCode(content)
  340. expect(bindings).toStrictEqual({
  341. foo: BindingTypes.PROPS,
  342. })
  343. })
  344. test('multi-variable declaration', () => {
  345. const { content } = compile(`
  346. <script setup>
  347. const { item } = defineProps(['item']),
  348. a = 1;
  349. </script>
  350. `)
  351. assertCode(content)
  352. expect(content).toMatch(`const a = 1;`)
  353. expect(content).toMatch(`props: ['item'],`)
  354. })
  355. // #6757
  356. test('multi-variable declaration fix #6757 ', () => {
  357. const { content } = compile(`
  358. <script setup>
  359. const a = 1,
  360. { item } = defineProps(['item']);
  361. </script>
  362. `)
  363. assertCode(content)
  364. expect(content).toMatch(`const a = 1;`)
  365. expect(content).toMatch(`props: ['item'],`)
  366. })
  367. // #7422
  368. test('multi-variable declaration fix #7422', () => {
  369. const { content } = compile(`
  370. <script setup>
  371. const { item } = defineProps(['item']),
  372. a = 0,
  373. b = 0;
  374. </script>
  375. `)
  376. assertCode(content)
  377. expect(content).toMatch(`const a = 0,`)
  378. expect(content).toMatch(`b = 0;`)
  379. expect(content).toMatch(`props: ['item'],`)
  380. })
  381. test('handle function parameters with same name as destructured props', () => {
  382. const { content } = compile(`
  383. <script setup>
  384. const { value } = defineProps()
  385. function test(value) {
  386. try {
  387. } catch {
  388. }
  389. }
  390. console.log(value)
  391. </script>
  392. `)
  393. assertCode(content)
  394. expect(content).toMatch(`console.log(__props.value)`)
  395. })
  396. test('defineProps/defineEmits in multi-variable declaration (full removal)', () => {
  397. const { content } = compile(`
  398. <script setup>
  399. const props = defineProps(['item']),
  400. emit = defineEmits(['a']);
  401. </script>
  402. `)
  403. assertCode(content)
  404. expect(content).toMatch(`props: ['item'],`)
  405. expect(content).toMatch(`emits: ['a'],`)
  406. })
  407. describe('errors', () => {
  408. test('should error on deep destructure', () => {
  409. expect(() =>
  410. compile(
  411. `<script setup>const { foo: [bar] } = defineProps(['foo'])</script>`,
  412. ),
  413. ).toThrow(`destructure does not support nested patterns`)
  414. expect(() =>
  415. compile(
  416. `<script setup>const { foo: { bar } } = defineProps(['foo'])</script>`,
  417. ),
  418. ).toThrow(`destructure does not support nested patterns`)
  419. })
  420. test('should error on computed key', () => {
  421. expect(() =>
  422. compile(
  423. `<script setup>const { [foo]: bar } = defineProps(['foo'])</script>`,
  424. ),
  425. ).toThrow(`destructure cannot use computed key`)
  426. })
  427. test('should warn when used with withDefaults', () => {
  428. compile(
  429. `<script setup lang="ts">
  430. const { foo } = withDefaults(defineProps<{ foo: string }>(), { foo: 'foo' })
  431. </script>`,
  432. )
  433. expect(
  434. `withDefaults() is unnecessary when using destructure`,
  435. ).toHaveBeenWarned()
  436. })
  437. test('should error if destructure reference local vars', () => {
  438. expect(() =>
  439. compile(
  440. `<script setup>
  441. let x = 1
  442. const {
  443. foo = () => x
  444. } = defineProps(['foo'])
  445. </script>`,
  446. ),
  447. ).toThrow(`cannot reference locally declared variables`)
  448. })
  449. test('should error if assignment to destructured prop binding', () => {
  450. expect(() =>
  451. compile(
  452. `<script setup>
  453. const { foo } = defineProps(['foo'])
  454. foo = 'bar'
  455. </script>`,
  456. ),
  457. ).toThrow(`Cannot assign to destructured props`)
  458. expect(() =>
  459. compile(
  460. `<script setup>
  461. let { foo } = defineProps(['foo'])
  462. foo = 'bar'
  463. </script>`,
  464. ),
  465. ).toThrow(`Cannot assign to destructured props`)
  466. })
  467. test('should error when passing destructured prop into certain methods', () => {
  468. expect(() =>
  469. compile(
  470. `<script setup>
  471. import { watch } from 'vue'
  472. const { foo } = defineProps(['foo'])
  473. watch(foo, () => {})
  474. </script>`,
  475. ),
  476. ).toThrow(
  477. `"foo" is a destructured prop and should not be passed directly to watch().`,
  478. )
  479. expect(() =>
  480. compile(
  481. `<script setup>
  482. import { watch as w } from 'vue'
  483. const { foo } = defineProps(['foo'])
  484. w(foo, () => {})
  485. </script>`,
  486. ),
  487. ).toThrow(
  488. `"foo" is a destructured prop and should not be passed directly to watch().`,
  489. )
  490. expect(() =>
  491. compile(
  492. `<script setup>
  493. import { toRef } from 'vue'
  494. const { foo } = defineProps(['foo'])
  495. toRef(foo)
  496. </script>`,
  497. ),
  498. ).toThrow(
  499. `"foo" is a destructured prop and should not be passed directly to toRef().`,
  500. )
  501. expect(() =>
  502. compile(
  503. `<script setup>
  504. import { toRef as r } from 'vue'
  505. const { foo } = defineProps(['foo'])
  506. r(foo)
  507. </script>`,
  508. ),
  509. ).toThrow(
  510. `"foo" is a destructured prop and should not be passed directly to toRef().`,
  511. )
  512. })
  513. // not comprehensive, but should help for most common cases
  514. test('should error if default value type does not match declared type', () => {
  515. expect(() =>
  516. compile(
  517. `<script setup lang="ts">
  518. const { foo = 'hello' } = defineProps<{ foo?: number }>()
  519. </script>`,
  520. ),
  521. ).toThrow(`Default value of prop "foo" does not match declared type.`)
  522. })
  523. // #8017
  524. test('should not throw an error if the variable is not a props', () => {
  525. expect(() =>
  526. compile(
  527. `<script setup lang='ts'>
  528. import { watch } from 'vue'
  529. const { userId } = defineProps({ userId: Number })
  530. const { error: e, info } = useRequest();
  531. watch(e, () => {});
  532. watch(info, () => {});
  533. </script>`,
  534. ),
  535. ).not.toThrowError()
  536. })
  537. })
  538. })