definePropsDestructure.spec.ts 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572
  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. `)
  188. expect(bindings).toStrictEqual({
  189. __propsAliases: {
  190. fooBar: 'foo:bar',
  191. },
  192. foo: BindingTypes.PROPS,
  193. bar: BindingTypes.PROPS,
  194. 'foo:bar': BindingTypes.PROPS,
  195. fooBar: BindingTypes.PROPS_ALIASED,
  196. 'onUpdate:modelValue': BindingTypes.PROPS,
  197. })
  198. expect(content).toMatch(`
  199. props: {
  200. foo: { type: Number, required: true, default: 1 },
  201. bar: { type: Number, required: true, default: 2 },
  202. "foo:bar": { type: String, required: true, default: 'foo-bar' },
  203. "onUpdate:modelValue": { type: Function, required: true }
  204. },`)
  205. assertCode(content)
  206. })
  207. test('default values w/ type declaration, prod mode', () => {
  208. const { content } = compile(
  209. `
  210. <script setup lang="ts">
  211. const { foo = 1, bar = {}, func = () => {} } = defineProps<{ foo?: number, bar?: object, baz?: any, boola?: boolean, boolb?: boolean | number, func?: Function }>()
  212. </script>
  213. `,
  214. { isProd: true },
  215. )
  216. assertCode(content)
  217. // literals can be used as-is, non-literals are always returned from a
  218. // function
  219. expect(content).toMatch(`props: {
  220. foo: { default: 1 },
  221. bar: { default: () => ({}) },
  222. baz: {},
  223. boola: { type: Boolean },
  224. boolb: { type: [Boolean, Number] },
  225. func: { type: Function, default: () => {} }
  226. }`)
  227. })
  228. test('with TSInstantiationExpression', () => {
  229. const { content } = compile(
  230. `
  231. <script setup lang="ts">
  232. type Foo = <T extends string | number>(data: T) => void
  233. const { value } = defineProps<{ value: Foo }>()
  234. const foo = value<123>
  235. </script>
  236. `,
  237. { isProd: true },
  238. )
  239. assertCode(content)
  240. expect(content).toMatch(`const foo = __props.value<123>`)
  241. })
  242. test('aliasing', () => {
  243. const { content, bindings } = compile(`
  244. <script setup>
  245. const { foo: bar } = defineProps(['foo'])
  246. let x = foo
  247. let y = bar
  248. </script>
  249. <template>{{ foo + bar }}</template>
  250. `)
  251. expect(content).not.toMatch(`const { foo: bar } =`)
  252. expect(content).toMatch(`let x = foo`) // should not process
  253. expect(content).toMatch(`let y = __props.foo`)
  254. // should convert bar to __props.foo in template expressions
  255. expect(content).toMatch(`_toDisplayString(__props.foo + __props.foo)`)
  256. assertCode(content)
  257. expect(bindings).toStrictEqual({
  258. x: BindingTypes.SETUP_LET,
  259. y: BindingTypes.SETUP_LET,
  260. foo: BindingTypes.PROPS,
  261. bar: BindingTypes.PROPS_ALIASED,
  262. __propsAliases: {
  263. bar: 'foo',
  264. },
  265. })
  266. })
  267. // #5425
  268. test('non-identifier prop names', () => {
  269. const { content, bindings } = compile(`
  270. <script setup>
  271. const { 'foo.bar': fooBar } = defineProps({ 'foo.bar': Function })
  272. let x = fooBar
  273. </script>
  274. <template>{{ fooBar }}</template>
  275. `)
  276. expect(content).toMatch(`x = __props["foo.bar"]`)
  277. expect(content).toMatch(`toDisplayString(__props["foo.bar"])`)
  278. assertCode(content)
  279. expect(bindings).toStrictEqual({
  280. x: BindingTypes.SETUP_LET,
  281. 'foo.bar': BindingTypes.PROPS,
  282. fooBar: BindingTypes.PROPS_ALIASED,
  283. __propsAliases: {
  284. fooBar: 'foo.bar',
  285. },
  286. })
  287. })
  288. test('rest spread', () => {
  289. const { content, bindings } = compile(`
  290. <script setup>
  291. const { foo, bar, ...rest } = defineProps(['foo', 'bar', 'baz'])
  292. </script>
  293. `)
  294. expect(content).toMatch(
  295. `const rest = _createPropsRestProxy(__props, ["foo","bar"])`,
  296. )
  297. assertCode(content)
  298. expect(bindings).toStrictEqual({
  299. foo: BindingTypes.PROPS,
  300. bar: BindingTypes.PROPS,
  301. baz: BindingTypes.PROPS,
  302. rest: BindingTypes.SETUP_REACTIVE_CONST,
  303. })
  304. })
  305. test('rest spread non-inline', () => {
  306. const { content, bindings } = compile(
  307. `
  308. <script setup>
  309. const { foo, ...rest } = defineProps(['foo', 'bar'])
  310. </script>
  311. <template>{{ rest.bar }}</template>
  312. `,
  313. { inlineTemplate: false },
  314. )
  315. expect(content).toMatch(
  316. `const rest = _createPropsRestProxy(__props, ["foo"])`,
  317. )
  318. assertCode(content)
  319. expect(bindings).toStrictEqual({
  320. foo: BindingTypes.PROPS,
  321. bar: BindingTypes.PROPS,
  322. rest: BindingTypes.SETUP_REACTIVE_CONST,
  323. })
  324. })
  325. // #6960
  326. test('computed static key', () => {
  327. const { content, bindings } = compile(`
  328. <script setup>
  329. const { ['foo']: foo } = defineProps(['foo'])
  330. console.log(foo)
  331. </script>
  332. <template>{{ foo }}</template>
  333. `)
  334. expect(content).not.toMatch(`const { foo } =`)
  335. expect(content).toMatch(`console.log(__props.foo)`)
  336. expect(content).toMatch(`_toDisplayString(__props.foo)`)
  337. assertCode(content)
  338. expect(bindings).toStrictEqual({
  339. foo: BindingTypes.PROPS,
  340. })
  341. })
  342. test('multi-variable declaration', () => {
  343. const { content } = compile(`
  344. <script setup>
  345. const { item } = defineProps(['item']),
  346. a = 1;
  347. </script>
  348. `)
  349. assertCode(content)
  350. expect(content).toMatch(`const a = 1;`)
  351. expect(content).toMatch(`props: ['item'],`)
  352. })
  353. // #6757
  354. test('multi-variable declaration fix #6757 ', () => {
  355. const { content } = compile(`
  356. <script setup>
  357. const a = 1,
  358. { item } = defineProps(['item']);
  359. </script>
  360. `)
  361. assertCode(content)
  362. expect(content).toMatch(`const a = 1;`)
  363. expect(content).toMatch(`props: ['item'],`)
  364. })
  365. // #7422
  366. test('multi-variable declaration fix #7422', () => {
  367. const { content } = compile(`
  368. <script setup>
  369. const { item } = defineProps(['item']),
  370. a = 0,
  371. b = 0;
  372. </script>
  373. `)
  374. assertCode(content)
  375. expect(content).toMatch(`const a = 0,`)
  376. expect(content).toMatch(`b = 0;`)
  377. expect(content).toMatch(`props: ['item'],`)
  378. })
  379. test('handle function parameters with same name as destructured props', () => {
  380. const { content } = compile(`
  381. <script setup>
  382. const { value } = defineProps()
  383. function test(value) {
  384. try {
  385. } catch {
  386. }
  387. }
  388. console.log(value)
  389. </script>
  390. `)
  391. assertCode(content)
  392. expect(content).toMatch(`console.log(__props.value)`)
  393. })
  394. test('defineProps/defineEmits in multi-variable declaration (full removal)', () => {
  395. const { content } = compile(`
  396. <script setup>
  397. const props = defineProps(['item']),
  398. emit = defineEmits(['a']);
  399. </script>
  400. `)
  401. assertCode(content)
  402. expect(content).toMatch(`props: ['item'],`)
  403. expect(content).toMatch(`emits: ['a'],`)
  404. })
  405. describe('errors', () => {
  406. test('should error on deep destructure', () => {
  407. expect(() =>
  408. compile(
  409. `<script setup>const { foo: [bar] } = defineProps(['foo'])</script>`,
  410. ),
  411. ).toThrow(`destructure does not support nested patterns`)
  412. expect(() =>
  413. compile(
  414. `<script setup>const { foo: { bar } } = defineProps(['foo'])</script>`,
  415. ),
  416. ).toThrow(`destructure does not support nested patterns`)
  417. })
  418. test('should error on computed key', () => {
  419. expect(() =>
  420. compile(
  421. `<script setup>const { [foo]: bar } = defineProps(['foo'])</script>`,
  422. ),
  423. ).toThrow(`destructure cannot use computed key`)
  424. })
  425. test('should warn when used with withDefaults', () => {
  426. compile(
  427. `<script setup lang="ts">
  428. const { foo } = withDefaults(defineProps<{ foo: string }>(), { foo: 'foo' })
  429. </script>`,
  430. )
  431. expect(
  432. `withDefaults() is unnecessary when using destructure`,
  433. ).toHaveBeenWarned()
  434. })
  435. test('should error if destructure reference local vars', () => {
  436. expect(() =>
  437. compile(
  438. `<script setup>
  439. let x = 1
  440. const {
  441. foo = () => x
  442. } = defineProps(['foo'])
  443. </script>`,
  444. ),
  445. ).toThrow(`cannot reference locally declared variables`)
  446. })
  447. test('should error if assignment to destructured prop binding', () => {
  448. expect(() =>
  449. compile(
  450. `<script setup>
  451. const { foo } = defineProps(['foo'])
  452. foo = 'bar'
  453. </script>`,
  454. ),
  455. ).toThrow(`Cannot assign to destructured props`)
  456. expect(() =>
  457. compile(
  458. `<script setup>
  459. let { foo } = defineProps(['foo'])
  460. foo = 'bar'
  461. </script>`,
  462. ),
  463. ).toThrow(`Cannot assign to destructured props`)
  464. })
  465. test('should error when passing destructured prop into certain methods', () => {
  466. expect(() =>
  467. compile(
  468. `<script setup>
  469. import { watch } from 'vue'
  470. const { foo } = defineProps(['foo'])
  471. watch(foo, () => {})
  472. </script>`,
  473. ),
  474. ).toThrow(
  475. `"foo" is a destructured prop and should not be passed directly to watch().`,
  476. )
  477. expect(() =>
  478. compile(
  479. `<script setup>
  480. import { watch as w } from 'vue'
  481. const { foo } = defineProps(['foo'])
  482. w(foo, () => {})
  483. </script>`,
  484. ),
  485. ).toThrow(
  486. `"foo" is a destructured prop and should not be passed directly to watch().`,
  487. )
  488. expect(() =>
  489. compile(
  490. `<script setup>
  491. import { toRef } from 'vue'
  492. const { foo } = defineProps(['foo'])
  493. toRef(foo)
  494. </script>`,
  495. ),
  496. ).toThrow(
  497. `"foo" is a destructured prop and should not be passed directly to toRef().`,
  498. )
  499. expect(() =>
  500. compile(
  501. `<script setup>
  502. import { toRef as r } from 'vue'
  503. const { foo } = defineProps(['foo'])
  504. r(foo)
  505. </script>`,
  506. ),
  507. ).toThrow(
  508. `"foo" is a destructured prop and should not be passed directly to toRef().`,
  509. )
  510. })
  511. // not comprehensive, but should help for most common cases
  512. test('should error if default value type does not match declared type', () => {
  513. expect(() =>
  514. compile(
  515. `<script setup lang="ts">
  516. const { foo = 'hello' } = defineProps<{ foo?: number }>()
  517. </script>`,
  518. ),
  519. ).toThrow(`Default value of prop "foo" does not match declared type.`)
  520. })
  521. // #8017
  522. test('should not throw an error if the variable is not a props', () => {
  523. expect(() =>
  524. compile(
  525. `<script setup lang='ts'>
  526. import { watch } from 'vue'
  527. const { userId } = defineProps({ userId: Number })
  528. const { error: e, info } = useRequest();
  529. watch(e, () => {});
  530. watch(info, () => {});
  531. </script>`,
  532. ),
  533. ).not.toThrowError()
  534. })
  535. })
  536. })