compileScript.spec.ts 35 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257
  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. expect(content).toMatch(`${shouldAsync ? `async ` : ``}setup(`)
  802. if (typeof expected === 'string') {
  803. expect(content).toMatch(expected)
  804. } else {
  805. expect(expected(content)).toBe(true)
  806. }
  807. }
  808. test('expression statement', () => {
  809. assertAwaitDetection(`await foo`, `await _withAsyncContext(foo)`)
  810. })
  811. test('variable', () => {
  812. assertAwaitDetection(
  813. `const a = 1 + (await foo)`,
  814. `1 + (await _withAsyncContext(foo))`
  815. )
  816. })
  817. test('ref', () => {
  818. assertAwaitDetection(
  819. `ref: a = 1 + (await foo)`,
  820. `1 + (await _withAsyncContext(foo))`
  821. )
  822. })
  823. test('nested statements', () => {
  824. assertAwaitDetection(`if (ok) { await foo } else { await bar }`, code => {
  825. return (
  826. code.includes(`await _withAsyncContext(foo)`) &&
  827. code.includes(`await _withAsyncContext(bar)`)
  828. )
  829. })
  830. })
  831. test('should ignore await inside functions', () => {
  832. // function declaration
  833. assertAwaitDetection(
  834. `async function foo() { await bar }`,
  835. `await bar`,
  836. false
  837. )
  838. // function expression
  839. assertAwaitDetection(
  840. `const foo = async () => { await bar }`,
  841. `await bar`,
  842. false
  843. )
  844. // object method
  845. assertAwaitDetection(
  846. `const obj = { async method() { await bar }}`,
  847. `await bar`,
  848. false
  849. )
  850. // class method
  851. assertAwaitDetection(
  852. `const cls = class Foo { async method() { await bar }}`,
  853. `await bar`,
  854. false
  855. )
  856. })
  857. })
  858. describe('errors', () => {
  859. test('<script> and <script setup> must have same lang', () => {
  860. expect(() =>
  861. compile(`<script>foo()</script><script setup lang="ts">bar()</script>`)
  862. ).toThrow(`<script> and <script setup> must have the same language type`)
  863. })
  864. const moduleErrorMsg = `cannot contain ES module exports`
  865. test('non-type named exports', () => {
  866. expect(() =>
  867. compile(`<script setup>
  868. export const a = 1
  869. </script>`)
  870. ).toThrow(moduleErrorMsg)
  871. expect(() =>
  872. compile(`<script setup>
  873. export * from './foo'
  874. </script>`)
  875. ).toThrow(moduleErrorMsg)
  876. expect(() =>
  877. compile(`<script setup>
  878. const bar = 1
  879. export { bar as default }
  880. </script>`)
  881. ).toThrow(moduleErrorMsg)
  882. })
  883. test('defineProps/Emit() w/ both type and non-type args', () => {
  884. expect(() => {
  885. compile(`<script setup lang="ts">
  886. defineProps<{}>({})
  887. </script>`)
  888. }).toThrow(`cannot accept both type and non-type arguments`)
  889. expect(() => {
  890. compile(`<script setup lang="ts">
  891. defineEmits<{}>({})
  892. </script>`)
  893. }).toThrow(`cannot accept both type and non-type arguments`)
  894. })
  895. test('defineProps/Emit() referencing local var', () => {
  896. expect(() =>
  897. compile(`<script setup>
  898. const bar = 1
  899. defineProps({
  900. foo: {
  901. default: () => bar
  902. }
  903. })
  904. </script>`)
  905. ).toThrow(`cannot reference locally declared variables`)
  906. expect(() =>
  907. compile(`<script setup>
  908. const bar = 'hello'
  909. defineEmits([bar])
  910. </script>`)
  911. ).toThrow(`cannot reference locally declared variables`)
  912. })
  913. test('should allow defineProps/Emit() referencing scope var', () => {
  914. assertCode(
  915. compile(`<script setup>
  916. const bar = 1
  917. defineProps({
  918. foo: {
  919. default: bar => bar + 1
  920. }
  921. })
  922. defineEmits({
  923. foo: bar => bar > 1
  924. })
  925. </script>`).content
  926. )
  927. })
  928. test('should allow defineProps/Emit() referencing imported binding', () => {
  929. assertCode(
  930. compile(`<script setup>
  931. import { bar } from './bar'
  932. defineProps({
  933. foo: {
  934. default: () => bar
  935. }
  936. })
  937. defineEmits({
  938. foo: () => bar > 1
  939. })
  940. </script>`).content
  941. )
  942. })
  943. })
  944. })
  945. describe('SFC analyze <script> bindings', () => {
  946. it('can parse decorators syntax in typescript block', () => {
  947. const { scriptAst } = compile(`
  948. <script lang="ts">
  949. import { Options, Vue } from 'vue-class-component';
  950. @Options({
  951. components: {
  952. HelloWorld,
  953. },
  954. props: ['foo', 'bar']
  955. })
  956. export default class Home extends Vue {}
  957. </script>
  958. `)
  959. expect(scriptAst).toBeDefined()
  960. })
  961. it('recognizes props array declaration', () => {
  962. const { bindings } = compile(`
  963. <script>
  964. export default {
  965. props: ['foo', 'bar']
  966. }
  967. </script>
  968. `)
  969. expect(bindings).toStrictEqual({
  970. foo: BindingTypes.PROPS,
  971. bar: BindingTypes.PROPS
  972. })
  973. expect(bindings!.__isScriptSetup).toBe(false)
  974. })
  975. it('recognizes props object declaration', () => {
  976. const { bindings } = compile(`
  977. <script>
  978. export default {
  979. props: {
  980. foo: String,
  981. bar: {
  982. type: String,
  983. },
  984. baz: null,
  985. qux: [String, Number]
  986. }
  987. }
  988. </script>
  989. `)
  990. expect(bindings).toStrictEqual({
  991. foo: BindingTypes.PROPS,
  992. bar: BindingTypes.PROPS,
  993. baz: BindingTypes.PROPS,
  994. qux: BindingTypes.PROPS
  995. })
  996. expect(bindings!.__isScriptSetup).toBe(false)
  997. })
  998. it('recognizes setup return', () => {
  999. const { bindings } = compile(`
  1000. <script>
  1001. const bar = 2
  1002. export default {
  1003. setup() {
  1004. return {
  1005. foo: 1,
  1006. bar
  1007. }
  1008. }
  1009. }
  1010. </script>
  1011. `)
  1012. expect(bindings).toStrictEqual({
  1013. foo: BindingTypes.SETUP_MAYBE_REF,
  1014. bar: BindingTypes.SETUP_MAYBE_REF
  1015. })
  1016. expect(bindings!.__isScriptSetup).toBe(false)
  1017. })
  1018. it('recognizes async setup return', () => {
  1019. const { bindings } = compile(`
  1020. <script>
  1021. const bar = 2
  1022. export default {
  1023. async setup() {
  1024. return {
  1025. foo: 1,
  1026. bar
  1027. }
  1028. }
  1029. }
  1030. </script>
  1031. `)
  1032. expect(bindings).toStrictEqual({
  1033. foo: BindingTypes.SETUP_MAYBE_REF,
  1034. bar: BindingTypes.SETUP_MAYBE_REF
  1035. })
  1036. expect(bindings!.__isScriptSetup).toBe(false)
  1037. })
  1038. it('recognizes data return', () => {
  1039. const { bindings } = compile(`
  1040. <script>
  1041. const bar = 2
  1042. export default {
  1043. data() {
  1044. return {
  1045. foo: null,
  1046. bar
  1047. }
  1048. }
  1049. }
  1050. </script>
  1051. `)
  1052. expect(bindings).toStrictEqual({
  1053. foo: BindingTypes.DATA,
  1054. bar: BindingTypes.DATA
  1055. })
  1056. })
  1057. it('recognizes methods', () => {
  1058. const { bindings } = compile(`
  1059. <script>
  1060. export default {
  1061. methods: {
  1062. foo() {}
  1063. }
  1064. }
  1065. </script>
  1066. `)
  1067. expect(bindings).toStrictEqual({ foo: BindingTypes.OPTIONS })
  1068. })
  1069. it('recognizes computeds', () => {
  1070. const { bindings } = compile(`
  1071. <script>
  1072. export default {
  1073. computed: {
  1074. foo() {},
  1075. bar: {
  1076. get() {},
  1077. set() {},
  1078. }
  1079. }
  1080. }
  1081. </script>
  1082. `)
  1083. expect(bindings).toStrictEqual({
  1084. foo: BindingTypes.OPTIONS,
  1085. bar: BindingTypes.OPTIONS
  1086. })
  1087. })
  1088. it('recognizes injections array declaration', () => {
  1089. const { bindings } = compile(`
  1090. <script>
  1091. export default {
  1092. inject: ['foo', 'bar']
  1093. }
  1094. </script>
  1095. `)
  1096. expect(bindings).toStrictEqual({
  1097. foo: BindingTypes.OPTIONS,
  1098. bar: BindingTypes.OPTIONS
  1099. })
  1100. })
  1101. it('recognizes injections object declaration', () => {
  1102. const { bindings } = compile(`
  1103. <script>
  1104. export default {
  1105. inject: {
  1106. foo: {},
  1107. bar: {},
  1108. }
  1109. }
  1110. </script>
  1111. `)
  1112. expect(bindings).toStrictEqual({
  1113. foo: BindingTypes.OPTIONS,
  1114. bar: BindingTypes.OPTIONS
  1115. })
  1116. })
  1117. it('works for mixed bindings', () => {
  1118. const { bindings } = compile(`
  1119. <script>
  1120. export default {
  1121. inject: ['foo'],
  1122. props: {
  1123. bar: String,
  1124. },
  1125. setup() {
  1126. return {
  1127. baz: null,
  1128. }
  1129. },
  1130. data() {
  1131. return {
  1132. qux: null
  1133. }
  1134. },
  1135. methods: {
  1136. quux() {}
  1137. },
  1138. computed: {
  1139. quuz() {}
  1140. }
  1141. }
  1142. </script>
  1143. `)
  1144. expect(bindings).toStrictEqual({
  1145. foo: BindingTypes.OPTIONS,
  1146. bar: BindingTypes.PROPS,
  1147. baz: BindingTypes.SETUP_MAYBE_REF,
  1148. qux: BindingTypes.DATA,
  1149. quux: BindingTypes.OPTIONS,
  1150. quuz: BindingTypes.OPTIONS
  1151. })
  1152. })
  1153. it('works for script setup', () => {
  1154. const { bindings } = compile(`
  1155. <script setup>
  1156. import { ref as r } from 'vue'
  1157. defineProps({
  1158. foo: String
  1159. })
  1160. const a = r(1)
  1161. let b = 2
  1162. const c = 3
  1163. const { d } = someFoo()
  1164. let { e } = someBar()
  1165. </script>
  1166. `)
  1167. expect(bindings).toStrictEqual({
  1168. r: BindingTypes.SETUP_CONST,
  1169. a: BindingTypes.SETUP_REF,
  1170. b: BindingTypes.SETUP_LET,
  1171. c: BindingTypes.SETUP_CONST,
  1172. d: BindingTypes.SETUP_MAYBE_REF,
  1173. e: BindingTypes.SETUP_LET,
  1174. foo: BindingTypes.PROPS
  1175. })
  1176. })
  1177. })