compileScript.spec.ts 35 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267
  1. import { BindingTypes } from '@vue/compiler-core'
  2. import { compileSFCScript as compile, assertCode } from './utils'
  3. describe('SFC compile <script setup>', () => {
  4. test('should expose top level declarations', () => {
  5. const { content } = compile(`
  6. <script setup>
  7. import { x } from './x'
  8. let a = 1
  9. const b = 2
  10. function c() {}
  11. class d {}
  12. </script>
  13. `)
  14. assertCode(content)
  15. expect(content).toMatch('return { a, b, c, d, x }')
  16. })
  17. test('defineProps()', () => {
  18. const { content, bindings } = compile(`
  19. <script setup>
  20. const props = defineProps({
  21. foo: String
  22. })
  23. const bar = 1
  24. </script>
  25. `)
  26. // should generate working code
  27. assertCode(content)
  28. // should anayze bindings
  29. expect(bindings).toStrictEqual({
  30. foo: BindingTypes.PROPS,
  31. bar: BindingTypes.SETUP_CONST,
  32. props: BindingTypes.SETUP_CONST
  33. })
  34. // should remove defineOptions import and call
  35. expect(content).not.toMatch('defineProps')
  36. // should generate correct setup signature
  37. expect(content).toMatch(`setup(__props, { expose }) {`)
  38. // should assign user identifier to it
  39. expect(content).toMatch(`const props = __props`)
  40. // should include context options in default export
  41. expect(content).toMatch(`export default {
  42. props: {
  43. foo: String
  44. },`)
  45. })
  46. test('defineProps w/ external definition', () => {
  47. const { content } = compile(`
  48. <script setup>
  49. import { propsModel } from './props'
  50. const props = defineProps(propsModel)
  51. </script>
  52. `)
  53. assertCode(content)
  54. expect(content).toMatch(`export default {
  55. props: propsModel,`)
  56. })
  57. test('defineEmit() (deprecated)', () => {
  58. const { content, bindings } = compile(`
  59. <script setup>
  60. const myEmit = defineEmit(['foo', 'bar'])
  61. </script>
  62. `)
  63. assertCode(content)
  64. expect(bindings).toStrictEqual({
  65. myEmit: BindingTypes.SETUP_CONST
  66. })
  67. // should remove defineOptions import and call
  68. expect(content).not.toMatch(/defineEmits?/)
  69. // should generate correct setup signature
  70. expect(content).toMatch(`setup(__props, { expose, emit: myEmit }) {`)
  71. // should include context options in default export
  72. expect(content).toMatch(`export default {
  73. emits: ['foo', 'bar'],`)
  74. })
  75. test('defineEmits()', () => {
  76. const { content, bindings } = compile(`
  77. <script setup>
  78. const myEmit = defineEmits(['foo', 'bar'])
  79. </script>
  80. `)
  81. assertCode(content)
  82. expect(bindings).toStrictEqual({
  83. myEmit: BindingTypes.SETUP_CONST
  84. })
  85. // should remove defineOptions import and call
  86. expect(content).not.toMatch('defineEmits')
  87. // should generate correct setup signature
  88. expect(content).toMatch(`setup(__props, { expose, emit: myEmit }) {`)
  89. // should include context options in default export
  90. expect(content).toMatch(`export default {
  91. emits: ['foo', 'bar'],`)
  92. })
  93. test('defineProps/defineEmits in multi-variable decalration', () => {
  94. const { content } = compile(`
  95. <script setup>
  96. const props = defineProps(['item']),
  97. a = 1,
  98. emit = defineEmits(['a']);
  99. </script>
  100. `)
  101. assertCode(content)
  102. expect(content).toMatch(`const a = 1;`) // test correct removal
  103. expect(content).toMatch(`props: ['item'],`)
  104. expect(content).toMatch(`emits: ['a'],`)
  105. })
  106. test('defineProps/defineEmits in multi-variable decalration (full removal)', () => {
  107. const { content } = compile(`
  108. <script setup>
  109. const props = defineProps(['item']),
  110. emit = defineEmits(['a']);
  111. </script>
  112. `)
  113. assertCode(content)
  114. expect(content).toMatch(`props: ['item'],`)
  115. expect(content).toMatch(`emits: ['a'],`)
  116. })
  117. test('defineExpose()', () => {
  118. const { content } = compile(`
  119. <script setup>
  120. defineExpose({ foo: 123 })
  121. </script>
  122. `)
  123. assertCode(content)
  124. // should remove defineOptions import and call
  125. expect(content).not.toMatch('defineExpose')
  126. // should generate correct setup signature
  127. expect(content).toMatch(`setup(__props, { expose }) {`)
  128. // should replace callee
  129. expect(content).toMatch(/\bexpose\(\{ foo: 123 \}\)/)
  130. })
  131. describe('<script> and <script setup> co-usage', () => {
  132. test('script first', () => {
  133. const { content } = compile(`
  134. <script>
  135. export const n = 1
  136. </script>
  137. <script setup>
  138. import { x } from './x'
  139. x()
  140. </script>
  141. `)
  142. assertCode(content)
  143. })
  144. test('script setup first', () => {
  145. const { content } = compile(`
  146. <script setup>
  147. import { x } from './x'
  148. x()
  149. </script>
  150. <script>
  151. export const n = 1
  152. </script>
  153. `)
  154. assertCode(content)
  155. })
  156. })
  157. describe('imports', () => {
  158. test('should hoist and expose imports', () => {
  159. assertCode(
  160. compile(`<script setup>
  161. import { ref } from 'vue'
  162. import 'foo/css'
  163. </script>`).content
  164. )
  165. })
  166. test('should extract comment for import or type declarations', () => {
  167. assertCode(
  168. compile(`
  169. <script setup>
  170. import a from 'a' // comment
  171. import b from 'b'
  172. </script>
  173. `).content
  174. )
  175. })
  176. // #2740
  177. test('should allow defineProps/Emit at the start of imports', () => {
  178. assertCode(
  179. compile(`<script setup>
  180. import { ref } from 'vue'
  181. defineProps(['foo'])
  182. defineEmits(['bar'])
  183. const r = ref(0)
  184. </script>`).content
  185. )
  186. })
  187. test('dedupe between user & helper', () => {
  188. const { content } = compile(
  189. `
  190. <script setup>
  191. import { ref } from 'vue'
  192. ref: foo = 1
  193. </script>
  194. `,
  195. { refSugar: true }
  196. )
  197. assertCode(content)
  198. expect(content).toMatch(`import { ref } from 'vue'`)
  199. })
  200. test('import dedupe between <script> and <script setup>', () => {
  201. const { content } = compile(`
  202. <script>
  203. import { x } from './x'
  204. </script>
  205. <script setup>
  206. import { x } from './x'
  207. x()
  208. </script>
  209. `)
  210. assertCode(content)
  211. expect(content.indexOf(`import { x }`)).toEqual(
  212. content.lastIndexOf(`import { x }`)
  213. )
  214. })
  215. })
  216. describe('inlineTemplate mode', () => {
  217. test('should work', () => {
  218. const { content } = compile(
  219. `
  220. <script setup>
  221. import { ref } from 'vue'
  222. const count = ref(0)
  223. </script>
  224. <template>
  225. <div>{{ count }}</div>
  226. <div>static</div>
  227. </template>
  228. `,
  229. { inlineTemplate: true }
  230. )
  231. // check snapshot and make sure helper imports and
  232. // hoists are placed correctly.
  233. assertCode(content)
  234. // in inline mode, no need to call expose() since nothing is exposed
  235. // anyway!
  236. expect(content).not.toMatch(`expose()`)
  237. })
  238. test('with defineExpose()', () => {
  239. const { content } = compile(
  240. `
  241. <script setup>
  242. const count = ref(0)
  243. defineExpose({ count })
  244. </script>
  245. `,
  246. { inlineTemplate: true }
  247. )
  248. assertCode(content)
  249. expect(content).toMatch(`setup(__props, { expose })`)
  250. expect(content).toMatch(`expose({ count })`)
  251. })
  252. test('referencing scope components and directives', () => {
  253. const { content } = compile(
  254. `
  255. <script setup>
  256. import ChildComp from './Child.vue'
  257. import SomeOtherComp from './Other.vue'
  258. import vMyDir from './my-dir'
  259. </script>
  260. <template>
  261. <div v-my-dir></div>
  262. <ChildComp/>
  263. <some-other-comp/>
  264. </template>
  265. `,
  266. { inlineTemplate: true }
  267. )
  268. expect(content).toMatch('[_unref(vMyDir)]')
  269. expect(content).toMatch('_createVNode(ChildComp)')
  270. // kebab-case component support
  271. expect(content).toMatch('_createVNode(SomeOtherComp)')
  272. assertCode(content)
  273. })
  274. test('avoid unref() when necessary', () => {
  275. // function, const, component import
  276. const { content } = compile(
  277. `<script setup>
  278. import { ref } from 'vue'
  279. import Foo, { bar } from './Foo.vue'
  280. import other from './util'
  281. const count = ref(0)
  282. const constant = {}
  283. const maybe = foo()
  284. let lett = 1
  285. function fn() {}
  286. </script>
  287. <template>
  288. <Foo>{{ bar }}</Foo>
  289. <div @click="fn">{{ count }} {{ constant }} {{ maybe }} {{ lett }} {{ other }}</div>
  290. </template>
  291. `,
  292. { inlineTemplate: true }
  293. )
  294. // no need to unref vue component import
  295. expect(content).toMatch(`createVNode(Foo,`)
  296. // #2699 should unref named imports from .vue
  297. expect(content).toMatch(`unref(bar)`)
  298. // should unref other imports
  299. expect(content).toMatch(`unref(other)`)
  300. // no need to unref constant literals
  301. expect(content).not.toMatch(`unref(constant)`)
  302. // should directly use .value for known refs
  303. expect(content).toMatch(`count.value`)
  304. // should unref() on const bindings that may be refs
  305. expect(content).toMatch(`unref(maybe)`)
  306. // should unref() on let bindings
  307. expect(content).toMatch(`unref(lett)`)
  308. // no need to unref function declarations
  309. expect(content).toMatch(`{ onClick: fn }`)
  310. // no need to mark constant fns in patch flag
  311. expect(content).not.toMatch(`PROPS`)
  312. assertCode(content)
  313. })
  314. test('v-model codegen', () => {
  315. const { content } = compile(
  316. `<script setup>
  317. import { ref } from 'vue'
  318. const count = ref(0)
  319. const maybe = foo()
  320. let lett = 1
  321. </script>
  322. <template>
  323. <input v-model="count">
  324. <input v-model="maybe">
  325. <input v-model="lett">
  326. </template>
  327. `,
  328. { inlineTemplate: true }
  329. )
  330. // known const ref: set value
  331. expect(content).toMatch(`count.value = $event`)
  332. // const but maybe ref: also assign .value directly since non-ref
  333. // won't work
  334. expect(content).toMatch(`maybe.value = $event`)
  335. // let: handle both cases
  336. expect(content).toMatch(
  337. `_isRef(lett) ? lett.value = $event : lett = $event`
  338. )
  339. assertCode(content)
  340. })
  341. test('template assignment expression codegen', () => {
  342. const { content } = compile(
  343. `<script setup>
  344. import { ref } from 'vue'
  345. const count = ref(0)
  346. const maybe = foo()
  347. let lett = 1
  348. let v = ref(1)
  349. </script>
  350. <template>
  351. <div @click="count = 1"/>
  352. <div @click="maybe = count"/>
  353. <div @click="lett = count"/>
  354. <div @click="v += 1"/>
  355. <div @click="v -= 1"/>
  356. </template>
  357. `,
  358. { inlineTemplate: true }
  359. )
  360. // known const ref: set value
  361. expect(content).toMatch(`count.value = 1`)
  362. // const but maybe ref: only assign after check
  363. expect(content).toMatch(`maybe.value = count.value`)
  364. // let: handle both cases
  365. expect(content).toMatch(
  366. `_isRef(lett) ? lett.value = count.value : lett = count.value`
  367. )
  368. expect(content).toMatch(`_isRef(v) ? v.value += 1 : v += 1`)
  369. expect(content).toMatch(`_isRef(v) ? v.value -= 1 : v -= 1`)
  370. assertCode(content)
  371. })
  372. test('template update expression codegen', () => {
  373. const { content } = compile(
  374. `<script setup>
  375. import { ref } from 'vue'
  376. const count = ref(0)
  377. const maybe = foo()
  378. let lett = 1
  379. </script>
  380. <template>
  381. <div @click="count++"/>
  382. <div @click="--count"/>
  383. <div @click="maybe++"/>
  384. <div @click="--maybe"/>
  385. <div @click="lett++"/>
  386. <div @click="--lett"/>
  387. </template>
  388. `,
  389. { inlineTemplate: true }
  390. )
  391. // known const ref: set value
  392. expect(content).toMatch(`count.value++`)
  393. expect(content).toMatch(`--count.value`)
  394. // const but maybe ref (non-ref case ignored)
  395. expect(content).toMatch(`maybe.value++`)
  396. expect(content).toMatch(`--maybe.value`)
  397. // let: handle both cases
  398. expect(content).toMatch(`_isRef(lett) ? lett.value++ : lett++`)
  399. expect(content).toMatch(`_isRef(lett) ? --lett.value : --lett`)
  400. assertCode(content)
  401. })
  402. test('template destructure assignment codegen', () => {
  403. const { content } = compile(
  404. `<script setup>
  405. import { ref } from 'vue'
  406. const val = {}
  407. const count = ref(0)
  408. const maybe = foo()
  409. let lett = 1
  410. </script>
  411. <template>
  412. <div @click="({ count } = val)"/>
  413. <div @click="[maybe] = val"/>
  414. <div @click="({ lett } = val)"/>
  415. </template>
  416. `,
  417. { inlineTemplate: true }
  418. )
  419. // known const ref: set value
  420. expect(content).toMatch(`({ count: count.value } = val)`)
  421. // const but maybe ref (non-ref case ignored)
  422. expect(content).toMatch(`[maybe.value] = val`)
  423. // let: assumes non-ref
  424. expect(content).toMatch(`{ lett: lett } = val`)
  425. assertCode(content)
  426. })
  427. test('ssr codegen', () => {
  428. const { content } = compile(
  429. `
  430. <script setup>
  431. import { ref } from 'vue'
  432. const count = ref(0)
  433. </script>
  434. <template>
  435. <div>{{ count }}</div>
  436. <div>static</div>
  437. </template>
  438. <style>
  439. div { color: v-bind(count) }
  440. </style>
  441. `,
  442. {
  443. inlineTemplate: true,
  444. templateOptions: {
  445. ssr: true
  446. }
  447. }
  448. )
  449. expect(content).toMatch(`\n __ssrInlineRender: true,\n`)
  450. expect(content).toMatch(`return (_ctx, _push`)
  451. expect(content).toMatch(`ssrInterpolate`)
  452. assertCode(content)
  453. })
  454. // _withId is only generated for backwards compat and is a noop when called
  455. // in module scope.
  456. // when inside setup(), currentInstance will be non-null and _withId will
  457. // no longer be noop and cause scopeId errors.
  458. // TODO: this test should no longer be necessary if we remove _withId
  459. // codegen in 3.1
  460. test('should not wrap render fn with withId when having scoped styles', async () => {
  461. const { content } = compile(
  462. `
  463. <script setup>
  464. const msg = 1
  465. </script>
  466. <template><h1>{{ msg }}</h1></template>
  467. <style scoped>
  468. h1 { color: red; }
  469. </style>
  470. `,
  471. {
  472. inlineTemplate: true
  473. }
  474. )
  475. expect(content).toMatch(`return (_ctx, _cache`)
  476. expect(content).not.toMatch(`_withId(`)
  477. assertCode(content)
  478. })
  479. })
  480. describe('with TypeScript', () => {
  481. test('hoist type declarations', () => {
  482. const { content } = compile(`
  483. <script setup lang="ts">
  484. export interface Foo {}
  485. type Bar = {}
  486. </script>`)
  487. assertCode(content)
  488. })
  489. test('defineProps/Emit w/ runtime options', () => {
  490. const { content } = compile(`
  491. <script setup lang="ts">
  492. const props = defineProps({ foo: String })
  493. const emit = defineEmits(['a', 'b'])
  494. </script>
  495. `)
  496. assertCode(content)
  497. expect(content).toMatch(`export default _defineComponent({
  498. props: { foo: String },
  499. emits: ['a', 'b'],
  500. setup(__props, { expose, emit }) {`)
  501. })
  502. test('defineProps w/ type', () => {
  503. const { content, bindings } = compile(`
  504. <script setup lang="ts">
  505. interface Test {}
  506. type Alias = number[]
  507. defineProps<{
  508. string: string
  509. number: number
  510. boolean: boolean
  511. object: object
  512. objectLiteral: { a: number }
  513. fn: (n: number) => void
  514. functionRef: Function
  515. objectRef: Object
  516. array: string[]
  517. arrayRef: Array<any>
  518. tuple: [number, number]
  519. set: Set<string>
  520. literal: 'foo'
  521. optional?: any
  522. recordRef: Record<string, null>
  523. interface: Test
  524. alias: Alias
  525. method(): void
  526. union: string | number
  527. literalUnion: 'foo' | 'bar'
  528. literalUnionMixed: 'foo' | 1 | boolean
  529. intersection: Test & {}
  530. }>()
  531. </script>`)
  532. assertCode(content)
  533. expect(content).toMatch(`string: { type: String, required: true }`)
  534. expect(content).toMatch(`number: { type: Number, required: true }`)
  535. expect(content).toMatch(`boolean: { type: Boolean, required: true }`)
  536. expect(content).toMatch(`object: { type: Object, required: true }`)
  537. expect(content).toMatch(`objectLiteral: { type: Object, required: true }`)
  538. expect(content).toMatch(`fn: { type: Function, required: true }`)
  539. expect(content).toMatch(`functionRef: { type: Function, required: true }`)
  540. expect(content).toMatch(`objectRef: { type: Object, required: true }`)
  541. expect(content).toMatch(`array: { type: Array, required: true }`)
  542. expect(content).toMatch(`arrayRef: { type: Array, required: true }`)
  543. expect(content).toMatch(`tuple: { type: Array, required: true }`)
  544. expect(content).toMatch(`set: { type: Set, required: true }`)
  545. expect(content).toMatch(`literal: { type: String, required: true }`)
  546. expect(content).toMatch(`optional: { type: null, required: false }`)
  547. expect(content).toMatch(`recordRef: { type: Object, required: true }`)
  548. expect(content).toMatch(`interface: { type: Object, required: true }`)
  549. expect(content).toMatch(`alias: { type: Array, required: true }`)
  550. expect(content).toMatch(`method: { type: Function, required: true }`)
  551. expect(content).toMatch(
  552. `union: { type: [String, Number], required: true }`
  553. )
  554. expect(content).toMatch(
  555. `literalUnion: { type: [String, String], required: true }`
  556. )
  557. expect(content).toMatch(
  558. `literalUnionMixed: { type: [String, Number, Boolean], required: true }`
  559. )
  560. expect(content).toMatch(`intersection: { type: Object, required: true }`)
  561. expect(bindings).toStrictEqual({
  562. string: BindingTypes.PROPS,
  563. number: BindingTypes.PROPS,
  564. boolean: BindingTypes.PROPS,
  565. object: BindingTypes.PROPS,
  566. objectLiteral: BindingTypes.PROPS,
  567. fn: BindingTypes.PROPS,
  568. functionRef: BindingTypes.PROPS,
  569. objectRef: BindingTypes.PROPS,
  570. array: BindingTypes.PROPS,
  571. arrayRef: BindingTypes.PROPS,
  572. tuple: BindingTypes.PROPS,
  573. set: BindingTypes.PROPS,
  574. literal: BindingTypes.PROPS,
  575. optional: BindingTypes.PROPS,
  576. recordRef: BindingTypes.PROPS,
  577. interface: BindingTypes.PROPS,
  578. alias: BindingTypes.PROPS,
  579. method: BindingTypes.PROPS,
  580. union: BindingTypes.PROPS,
  581. literalUnion: BindingTypes.PROPS,
  582. literalUnionMixed: BindingTypes.PROPS,
  583. intersection: BindingTypes.PROPS
  584. })
  585. })
  586. test('defineProps w/ interface', () => {
  587. const { content, bindings } = compile(`
  588. <script setup lang="ts">
  589. interface Props { x?: number }
  590. defineProps<Props>()
  591. </script>
  592. `)
  593. assertCode(content)
  594. expect(content).toMatch(`x: { type: Number, required: false }`)
  595. expect(bindings).toStrictEqual({
  596. x: BindingTypes.PROPS
  597. })
  598. })
  599. test('defineProps w/ exported interface', () => {
  600. const { content, bindings } = compile(`
  601. <script setup lang="ts">
  602. export interface Props { x?: number }
  603. defineProps<Props>()
  604. </script>
  605. `)
  606. assertCode(content)
  607. expect(content).toMatch(`x: { type: Number, required: false }`)
  608. expect(bindings).toStrictEqual({
  609. x: BindingTypes.PROPS
  610. })
  611. })
  612. test('defineProps w/ type alias', () => {
  613. const { content, bindings } = compile(`
  614. <script setup lang="ts">
  615. type Props = { x?: number }
  616. defineProps<Props>()
  617. </script>
  618. `)
  619. assertCode(content)
  620. expect(content).toMatch(`x: { type: Number, required: false }`)
  621. expect(bindings).toStrictEqual({
  622. x: BindingTypes.PROPS
  623. })
  624. })
  625. test('defineProps w/ exported type alias', () => {
  626. const { content, bindings } = compile(`
  627. <script setup lang="ts">
  628. export type Props = { x?: number }
  629. defineProps<Props>()
  630. </script>
  631. `)
  632. assertCode(content)
  633. expect(content).toMatch(`x: { type: Number, required: false }`)
  634. expect(bindings).toStrictEqual({
  635. x: BindingTypes.PROPS
  636. })
  637. })
  638. test('withDefaults (static)', () => {
  639. const { content, bindings } = compile(`
  640. <script setup lang="ts">
  641. const props = withDefaults(defineProps<{
  642. foo?: string
  643. bar?: number
  644. }>(), {
  645. foo: 'hi'
  646. })
  647. </script>
  648. `)
  649. assertCode(content)
  650. expect(content).toMatch(
  651. `foo: { type: String, required: false, default: 'hi' }`
  652. )
  653. expect(content).toMatch(`bar: { type: Number, required: false }`)
  654. expect(content).toMatch(`const props = __props`)
  655. expect(bindings).toStrictEqual({
  656. foo: BindingTypes.PROPS,
  657. bar: BindingTypes.PROPS,
  658. props: BindingTypes.SETUP_CONST
  659. })
  660. })
  661. test('withDefaults (dynamic)', () => {
  662. const { content } = compile(`
  663. <script setup lang="ts">
  664. import { defaults } from './foo'
  665. const props = withDefaults(defineProps<{
  666. foo?: string
  667. bar?: number
  668. }>(), { ...defaults })
  669. </script>
  670. `)
  671. assertCode(content)
  672. expect(content).toMatch(`import { mergeDefaults as _mergeDefaults`)
  673. expect(content).toMatch(
  674. `
  675. _mergeDefaults({
  676. foo: { type: String, required: false },
  677. bar: { type: Number, required: false }
  678. }, { ...defaults })`.trim()
  679. )
  680. })
  681. test('defineEmits w/ type', () => {
  682. const { content } = compile(`
  683. <script setup lang="ts">
  684. const emit = defineEmits<(e: 'foo' | 'bar') => void>()
  685. </script>
  686. `)
  687. assertCode(content)
  688. expect(content).toMatch(`emit: ((e: 'foo' | 'bar') => void),`)
  689. expect(content).toMatch(`emits: ["foo", "bar"] as unknown as undefined`)
  690. })
  691. test('defineEmits w/ type (union)', () => {
  692. const type = `((e: 'foo' | 'bar') => void) | ((e: 'baz', id: number) => void)`
  693. expect(() =>
  694. compile(`
  695. <script setup lang="ts">
  696. const emit = defineEmits<${type}>()
  697. </script>
  698. `)
  699. ).toThrow()
  700. })
  701. test('defineEmits w/ type (type literal w/ call signatures)', () => {
  702. const type = `{(e: 'foo' | 'bar'): void; (e: 'baz', id: number): void;}`
  703. const { content } = compile(`
  704. <script setup lang="ts">
  705. const emit = defineEmits<${type}>()
  706. </script>
  707. `)
  708. assertCode(content)
  709. expect(content).toMatch(`emit: (${type}),`)
  710. expect(content).toMatch(
  711. `emits: ["foo", "bar", "baz"] as unknown as undefined`
  712. )
  713. })
  714. test('defineEmits w/ type (interface)', () => {
  715. const { content } = compile(`
  716. <script setup lang="ts">
  717. interface Emits { (e: 'foo' | 'bar'): void }
  718. const emit = defineEmits<Emits>()
  719. </script>
  720. `)
  721. assertCode(content)
  722. expect(content).toMatch(`emit: ({ (e: 'foo' | 'bar'): void }),`)
  723. expect(content).toMatch(`emits: ["foo", "bar"] as unknown as undefined`)
  724. })
  725. test('defineEmits w/ type (exported interface)', () => {
  726. const { content } = compile(`
  727. <script setup lang="ts">
  728. export interface Emits { (e: 'foo' | 'bar'): void }
  729. const emit = defineEmits<Emits>()
  730. </script>
  731. `)
  732. assertCode(content)
  733. expect(content).toMatch(`emit: ({ (e: 'foo' | 'bar'): void }),`)
  734. expect(content).toMatch(`emits: ["foo", "bar"] as unknown as undefined`)
  735. })
  736. test('defineEmits w/ type (type alias)', () => {
  737. const { content } = compile(`
  738. <script setup lang="ts">
  739. type Emits = { (e: 'foo' | 'bar'): void }
  740. const emit = defineEmits<Emits>()
  741. </script>
  742. `)
  743. assertCode(content)
  744. expect(content).toMatch(`emit: ({ (e: 'foo' | 'bar'): void }),`)
  745. expect(content).toMatch(`emits: ["foo", "bar"] as unknown as undefined`)
  746. })
  747. test('defineEmits w/ type (exported type alias)', () => {
  748. const { content } = compile(`
  749. <script setup lang="ts">
  750. export type Emits = { (e: 'foo' | 'bar'): void }
  751. const emit = defineEmits<Emits>()
  752. </script>
  753. `)
  754. assertCode(content)
  755. expect(content).toMatch(`emit: ({ (e: 'foo' | 'bar'): void }),`)
  756. expect(content).toMatch(`emits: ["foo", "bar"] as unknown as undefined`)
  757. })
  758. test('defineEmits w/ type (referenced function type)', () => {
  759. const { content } = compile(`
  760. <script setup lang="ts">
  761. type Emits = (e: 'foo' | 'bar') => void
  762. const emit = defineEmits<Emits>()
  763. </script>
  764. `)
  765. assertCode(content)
  766. expect(content).toMatch(`emit: ((e: 'foo' | 'bar') => void),`)
  767. expect(content).toMatch(`emits: ["foo", "bar"] as unknown as undefined`)
  768. })
  769. test('defineEmits w/ type (referenced exported function type)', () => {
  770. const { content } = compile(`
  771. <script setup lang="ts">
  772. export type Emits = (e: 'foo' | 'bar') => void
  773. const emit = defineEmits<Emits>()
  774. </script>
  775. `)
  776. assertCode(content)
  777. expect(content).toMatch(`emit: ((e: 'foo' | 'bar') => void),`)
  778. expect(content).toMatch(`emits: ["foo", "bar"] as unknown as undefined`)
  779. })
  780. test('runtime Enum', () => {
  781. const { content, bindings } = compile(
  782. `<script setup lang="ts">
  783. enum Foo { A = 123 }
  784. </script>`
  785. )
  786. assertCode(content)
  787. expect(bindings).toStrictEqual({
  788. Foo: BindingTypes.SETUP_CONST
  789. })
  790. })
  791. })
  792. describe('async/await detection', () => {
  793. function assertAwaitDetection(
  794. code: string,
  795. expected: string | ((content: string) => boolean),
  796. shouldAsync = true
  797. ) {
  798. const { content } = compile(`<script setup>${code}</script>`, {
  799. refSugar: true
  800. })
  801. if (shouldAsync) {
  802. expect(content).toMatch(`let __temp, __restore`)
  803. }
  804. expect(content).toMatch(`${shouldAsync ? `async ` : ``}setup(`)
  805. if (typeof expected === 'string') {
  806. expect(content).toMatch(expected)
  807. } else {
  808. expect(expected(content)).toBe(true)
  809. }
  810. }
  811. test('expression statement', () => {
  812. assertAwaitDetection(
  813. `await foo`,
  814. `;(([__temp,__restore]=_withAsyncContext(()=>(foo))),__temp=await __temp,__restore())`
  815. )
  816. })
  817. test('variable', () => {
  818. assertAwaitDetection(
  819. `const a = 1 + (await foo)`,
  820. `1 + ((([__temp,__restore]=_withAsyncContext(()=>(foo))),__temp=await __temp,__restore(),__temp))`
  821. )
  822. })
  823. test('ref', () => {
  824. assertAwaitDetection(
  825. `ref: a = 1 + (await foo)`,
  826. `1 + ((([__temp,__restore]=_withAsyncContext(()=>(foo))),__temp=await __temp,__restore(),__temp))`
  827. )
  828. })
  829. test('nested statements', () => {
  830. assertAwaitDetection(`if (ok) { await foo } else { await bar }`, code => {
  831. return (
  832. code.includes(
  833. `;(([__temp,__restore]=_withAsyncContext(()=>(foo))),__temp=await __temp,__restore())`
  834. ) &&
  835. code.includes(
  836. `;(([__temp,__restore]=_withAsyncContext(()=>(bar))),__temp=await __temp,__restore())`
  837. )
  838. )
  839. })
  840. })
  841. test('should ignore await inside functions', () => {
  842. // function declaration
  843. assertAwaitDetection(
  844. `async function foo() { await bar }`,
  845. `await bar`,
  846. false
  847. )
  848. // function expression
  849. assertAwaitDetection(
  850. `const foo = async () => { await bar }`,
  851. `await bar`,
  852. false
  853. )
  854. // object method
  855. assertAwaitDetection(
  856. `const obj = { async method() { await bar }}`,
  857. `await bar`,
  858. false
  859. )
  860. // class method
  861. assertAwaitDetection(
  862. `const cls = class Foo { async method() { await bar }}`,
  863. `await bar`,
  864. false
  865. )
  866. })
  867. })
  868. describe('errors', () => {
  869. test('<script> and <script setup> must have same lang', () => {
  870. expect(() =>
  871. compile(`<script>foo()</script><script setup lang="ts">bar()</script>`)
  872. ).toThrow(`<script> and <script setup> must have the same language type`)
  873. })
  874. const moduleErrorMsg = `cannot contain ES module exports`
  875. test('non-type named exports', () => {
  876. expect(() =>
  877. compile(`<script setup>
  878. export const a = 1
  879. </script>`)
  880. ).toThrow(moduleErrorMsg)
  881. expect(() =>
  882. compile(`<script setup>
  883. export * from './foo'
  884. </script>`)
  885. ).toThrow(moduleErrorMsg)
  886. expect(() =>
  887. compile(`<script setup>
  888. const bar = 1
  889. export { bar as default }
  890. </script>`)
  891. ).toThrow(moduleErrorMsg)
  892. })
  893. test('defineProps/Emit() w/ both type and non-type args', () => {
  894. expect(() => {
  895. compile(`<script setup lang="ts">
  896. defineProps<{}>({})
  897. </script>`)
  898. }).toThrow(`cannot accept both type and non-type arguments`)
  899. expect(() => {
  900. compile(`<script setup lang="ts">
  901. defineEmits<{}>({})
  902. </script>`)
  903. }).toThrow(`cannot accept both type and non-type arguments`)
  904. })
  905. test('defineProps/Emit() referencing local var', () => {
  906. expect(() =>
  907. compile(`<script setup>
  908. const bar = 1
  909. defineProps({
  910. foo: {
  911. default: () => bar
  912. }
  913. })
  914. </script>`)
  915. ).toThrow(`cannot reference locally declared variables`)
  916. expect(() =>
  917. compile(`<script setup>
  918. const bar = 'hello'
  919. defineEmits([bar])
  920. </script>`)
  921. ).toThrow(`cannot reference locally declared variables`)
  922. })
  923. test('should allow defineProps/Emit() referencing scope var', () => {
  924. assertCode(
  925. compile(`<script setup>
  926. const bar = 1
  927. defineProps({
  928. foo: {
  929. default: bar => bar + 1
  930. }
  931. })
  932. defineEmits({
  933. foo: bar => bar > 1
  934. })
  935. </script>`).content
  936. )
  937. })
  938. test('should allow defineProps/Emit() referencing imported binding', () => {
  939. assertCode(
  940. compile(`<script setup>
  941. import { bar } from './bar'
  942. defineProps({
  943. foo: {
  944. default: () => bar
  945. }
  946. })
  947. defineEmits({
  948. foo: () => bar > 1
  949. })
  950. </script>`).content
  951. )
  952. })
  953. })
  954. })
  955. describe('SFC analyze <script> bindings', () => {
  956. it('can parse decorators syntax in typescript block', () => {
  957. const { scriptAst } = compile(`
  958. <script lang="ts">
  959. import { Options, Vue } from 'vue-class-component';
  960. @Options({
  961. components: {
  962. HelloWorld,
  963. },
  964. props: ['foo', 'bar']
  965. })
  966. export default class Home extends Vue {}
  967. </script>
  968. `)
  969. expect(scriptAst).toBeDefined()
  970. })
  971. it('recognizes props array declaration', () => {
  972. const { bindings } = compile(`
  973. <script>
  974. export default {
  975. props: ['foo', 'bar']
  976. }
  977. </script>
  978. `)
  979. expect(bindings).toStrictEqual({
  980. foo: BindingTypes.PROPS,
  981. bar: BindingTypes.PROPS
  982. })
  983. expect(bindings!.__isScriptSetup).toBe(false)
  984. })
  985. it('recognizes props object declaration', () => {
  986. const { bindings } = compile(`
  987. <script>
  988. export default {
  989. props: {
  990. foo: String,
  991. bar: {
  992. type: String,
  993. },
  994. baz: null,
  995. qux: [String, Number]
  996. }
  997. }
  998. </script>
  999. `)
  1000. expect(bindings).toStrictEqual({
  1001. foo: BindingTypes.PROPS,
  1002. bar: BindingTypes.PROPS,
  1003. baz: BindingTypes.PROPS,
  1004. qux: BindingTypes.PROPS
  1005. })
  1006. expect(bindings!.__isScriptSetup).toBe(false)
  1007. })
  1008. it('recognizes setup return', () => {
  1009. const { bindings } = compile(`
  1010. <script>
  1011. const bar = 2
  1012. export default {
  1013. setup() {
  1014. return {
  1015. foo: 1,
  1016. bar
  1017. }
  1018. }
  1019. }
  1020. </script>
  1021. `)
  1022. expect(bindings).toStrictEqual({
  1023. foo: BindingTypes.SETUP_MAYBE_REF,
  1024. bar: BindingTypes.SETUP_MAYBE_REF
  1025. })
  1026. expect(bindings!.__isScriptSetup).toBe(false)
  1027. })
  1028. it('recognizes async setup return', () => {
  1029. const { bindings } = compile(`
  1030. <script>
  1031. const bar = 2
  1032. export default {
  1033. async setup() {
  1034. return {
  1035. foo: 1,
  1036. bar
  1037. }
  1038. }
  1039. }
  1040. </script>
  1041. `)
  1042. expect(bindings).toStrictEqual({
  1043. foo: BindingTypes.SETUP_MAYBE_REF,
  1044. bar: BindingTypes.SETUP_MAYBE_REF
  1045. })
  1046. expect(bindings!.__isScriptSetup).toBe(false)
  1047. })
  1048. it('recognizes data return', () => {
  1049. const { bindings } = compile(`
  1050. <script>
  1051. const bar = 2
  1052. export default {
  1053. data() {
  1054. return {
  1055. foo: null,
  1056. bar
  1057. }
  1058. }
  1059. }
  1060. </script>
  1061. `)
  1062. expect(bindings).toStrictEqual({
  1063. foo: BindingTypes.DATA,
  1064. bar: BindingTypes.DATA
  1065. })
  1066. })
  1067. it('recognizes methods', () => {
  1068. const { bindings } = compile(`
  1069. <script>
  1070. export default {
  1071. methods: {
  1072. foo() {}
  1073. }
  1074. }
  1075. </script>
  1076. `)
  1077. expect(bindings).toStrictEqual({ foo: BindingTypes.OPTIONS })
  1078. })
  1079. it('recognizes computeds', () => {
  1080. const { bindings } = compile(`
  1081. <script>
  1082. export default {
  1083. computed: {
  1084. foo() {},
  1085. bar: {
  1086. get() {},
  1087. set() {},
  1088. }
  1089. }
  1090. }
  1091. </script>
  1092. `)
  1093. expect(bindings).toStrictEqual({
  1094. foo: BindingTypes.OPTIONS,
  1095. bar: BindingTypes.OPTIONS
  1096. })
  1097. })
  1098. it('recognizes injections array declaration', () => {
  1099. const { bindings } = compile(`
  1100. <script>
  1101. export default {
  1102. inject: ['foo', 'bar']
  1103. }
  1104. </script>
  1105. `)
  1106. expect(bindings).toStrictEqual({
  1107. foo: BindingTypes.OPTIONS,
  1108. bar: BindingTypes.OPTIONS
  1109. })
  1110. })
  1111. it('recognizes injections object declaration', () => {
  1112. const { bindings } = compile(`
  1113. <script>
  1114. export default {
  1115. inject: {
  1116. foo: {},
  1117. bar: {},
  1118. }
  1119. }
  1120. </script>
  1121. `)
  1122. expect(bindings).toStrictEqual({
  1123. foo: BindingTypes.OPTIONS,
  1124. bar: BindingTypes.OPTIONS
  1125. })
  1126. })
  1127. it('works for mixed bindings', () => {
  1128. const { bindings } = compile(`
  1129. <script>
  1130. export default {
  1131. inject: ['foo'],
  1132. props: {
  1133. bar: String,
  1134. },
  1135. setup() {
  1136. return {
  1137. baz: null,
  1138. }
  1139. },
  1140. data() {
  1141. return {
  1142. qux: null
  1143. }
  1144. },
  1145. methods: {
  1146. quux() {}
  1147. },
  1148. computed: {
  1149. quuz() {}
  1150. }
  1151. }
  1152. </script>
  1153. `)
  1154. expect(bindings).toStrictEqual({
  1155. foo: BindingTypes.OPTIONS,
  1156. bar: BindingTypes.PROPS,
  1157. baz: BindingTypes.SETUP_MAYBE_REF,
  1158. qux: BindingTypes.DATA,
  1159. quux: BindingTypes.OPTIONS,
  1160. quuz: BindingTypes.OPTIONS
  1161. })
  1162. })
  1163. it('works for script setup', () => {
  1164. const { bindings } = compile(`
  1165. <script setup>
  1166. import { ref as r } from 'vue'
  1167. defineProps({
  1168. foo: String
  1169. })
  1170. const a = r(1)
  1171. let b = 2
  1172. const c = 3
  1173. const { d } = someFoo()
  1174. let { e } = someBar()
  1175. </script>
  1176. `)
  1177. expect(bindings).toStrictEqual({
  1178. r: BindingTypes.SETUP_CONST,
  1179. a: BindingTypes.SETUP_REF,
  1180. b: BindingTypes.SETUP_LET,
  1181. c: BindingTypes.SETUP_CONST,
  1182. d: BindingTypes.SETUP_MAYBE_REF,
  1183. e: BindingTypes.SETUP_LET,
  1184. foo: BindingTypes.PROPS
  1185. })
  1186. })
  1187. })