compileScript.spec.ts 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782
  1. import { parse, SFCScriptCompileOptions, compileScript } from '../src'
  2. import { parse as babelParse } from '@babel/parser'
  3. import { babelParserDefaultPlugins } from '@vue/shared'
  4. function compile(src: string, options?: SFCScriptCompileOptions) {
  5. const { descriptor } = parse(src)
  6. return compileScript(descriptor, options)
  7. }
  8. function assertCode(code: string) {
  9. // parse the generated code to make sure it is valid
  10. try {
  11. babelParse(code, {
  12. sourceType: 'module',
  13. plugins: [...babelParserDefaultPlugins, 'typescript']
  14. })
  15. } catch (e) {
  16. console.log(code)
  17. throw e
  18. }
  19. expect(code).toMatchSnapshot()
  20. }
  21. describe('SFC compile <script setup>', () => {
  22. test('explicit setup signature', () => {
  23. assertCode(
  24. compile(`<script setup="props, { emit }">emit('foo')</script>`).content
  25. )
  26. })
  27. test('should expose top level declarations', () => {
  28. const { content } = compile(`
  29. <script setup>
  30. import { x } from './x'
  31. let a = 1
  32. const b = 2
  33. function c() {}
  34. class d {}
  35. </script>
  36. `)
  37. assertCode(content)
  38. expect(content).toMatch('return { x, a, b, c, d }')
  39. })
  40. describe('imports', () => {
  41. test('should hoist and expose imports', () => {
  42. assertCode(
  43. compile(`<script setup>import { ref } from 'vue'</script>`).content
  44. )
  45. })
  46. test('should extract comment for import or type declarations', () => {
  47. assertCode(
  48. compile(`<script setup>
  49. import a from 'a' // comment
  50. import b from 'b'
  51. </script>`).content
  52. )
  53. })
  54. test('dedupe between user & helper', () => {
  55. const { content } = compile(`<script setup>
  56. import { ref } from 'vue'
  57. ref: foo = 1
  58. </script>`)
  59. assertCode(content)
  60. expect(content).toMatch(`import { ref } from 'vue'`)
  61. })
  62. test('import dedupe between <script> and <script setup>', () => {
  63. const { content } = compile(`
  64. <script>
  65. import { x } from './x'
  66. </script>
  67. <script setup>
  68. import { x } from './x'
  69. x()
  70. </script>
  71. `)
  72. assertCode(content)
  73. expect(content.indexOf(`import { x }`)).toEqual(
  74. content.lastIndexOf(`import { x }`)
  75. )
  76. })
  77. })
  78. describe('<script setup lang="ts">', () => {
  79. test('hoist type declarations', () => {
  80. const { content } = compile(`
  81. <script setup lang="ts">
  82. export interface Foo {}
  83. type Bar = {}
  84. </script>`)
  85. assertCode(content)
  86. })
  87. test('extract props', () => {
  88. const { content } = compile(`
  89. <script setup="myProps" lang="ts">
  90. interface Test {}
  91. type Alias = number[]
  92. declare const myProps: {
  93. string: string
  94. number: number
  95. boolean: boolean
  96. object: object
  97. objectLiteral: { a: number }
  98. fn: (n: number) => void
  99. functionRef: Function
  100. objectRef: Object
  101. array: string[]
  102. arrayRef: Array<any>
  103. tuple: [number, number]
  104. set: Set<string>
  105. literal: 'foo'
  106. optional?: any
  107. recordRef: Record<string, null>
  108. interface: Test
  109. alias: Alias
  110. union: string | number
  111. literalUnion: 'foo' | 'bar'
  112. literalUnionMixed: 'foo' | 1 | boolean
  113. intersection: Test & {}
  114. }
  115. </script>`)
  116. assertCode(content)
  117. expect(content).toMatch(`string: { type: String, required: true }`)
  118. expect(content).toMatch(`number: { type: Number, required: true }`)
  119. expect(content).toMatch(`boolean: { type: Boolean, required: true }`)
  120. expect(content).toMatch(`object: { type: Object, required: true }`)
  121. expect(content).toMatch(`objectLiteral: { type: Object, required: true }`)
  122. expect(content).toMatch(`fn: { type: Function, required: true }`)
  123. expect(content).toMatch(`functionRef: { type: Function, required: true }`)
  124. expect(content).toMatch(`objectRef: { type: Object, required: true }`)
  125. expect(content).toMatch(`array: { type: Array, required: true }`)
  126. expect(content).toMatch(`arrayRef: { type: Array, required: true }`)
  127. expect(content).toMatch(`tuple: { type: Array, required: true }`)
  128. expect(content).toMatch(`set: { type: Set, required: true }`)
  129. expect(content).toMatch(`literal: { type: String, required: true }`)
  130. expect(content).toMatch(`optional: { type: null, required: false }`)
  131. expect(content).toMatch(`recordRef: { type: Object, required: true }`)
  132. expect(content).toMatch(`interface: { type: Object, required: true }`)
  133. expect(content).toMatch(`alias: { type: Array, required: true }`)
  134. expect(content).toMatch(
  135. `union: { type: [String, Number], required: true }`
  136. )
  137. expect(content).toMatch(
  138. `literalUnion: { type: [String, String], required: true }`
  139. )
  140. expect(content).toMatch(
  141. `literalUnionMixed: { type: [String, Number, Boolean], required: true }`
  142. )
  143. expect(content).toMatch(`intersection: { type: Object, required: true }`)
  144. })
  145. test('extract emits', () => {
  146. const { content } = compile(`
  147. <script setup="_, { emit: myEmit }" lang="ts">
  148. declare function myEmit(e: 'foo' | 'bar'): void
  149. declare function myEmit(e: 'baz', id: number): void
  150. </script>
  151. `)
  152. assertCode(content)
  153. expect(content).toMatch(
  154. `declare function __emit__(e: 'foo' | 'bar'): void`
  155. )
  156. expect(content).toMatch(
  157. `declare function __emit__(e: 'baz', id: number): void`
  158. )
  159. expect(content).toMatch(
  160. `emits: ["foo", "bar", "baz"] as unknown as undefined`
  161. )
  162. })
  163. })
  164. describe('CSS vars injection', () => {
  165. test('<script> w/ no default export', () => {
  166. assertCode(
  167. compile(
  168. `<script>const a = 1</script>\n` +
  169. `<style vars="{ color }">div{ color: var(--color); }</style>`
  170. ).content
  171. )
  172. })
  173. test('<script> w/ default export', () => {
  174. assertCode(
  175. compile(
  176. `<script>export default { setup() {} }</script>\n` +
  177. `<style vars="{ color }">div{ color: var(--color); }</style>`
  178. ).content
  179. )
  180. })
  181. test('<script> w/ default export in strings/comments', () => {
  182. assertCode(
  183. compile(
  184. `<script>
  185. // export default {}
  186. export default {}
  187. </script>\n` +
  188. `<style vars="{ color }">div{ color: var(--color); }</style>`
  189. ).content
  190. )
  191. })
  192. test('w/ <script setup>', () => {
  193. assertCode(
  194. compile(
  195. `<script setup>const color = 'red'</script>\n` +
  196. `<style vars="{ color }">div{ color: var(--color); }</style>`
  197. ).content
  198. )
  199. })
  200. })
  201. describe('async/await detection', () => {
  202. function assertAwaitDetection(code: string, shouldAsync = true) {
  203. const { content } = compile(`<script setup>${code}</script>`)
  204. expect(content).toMatch(
  205. `export ${shouldAsync ? `async ` : ``}function setup`
  206. )
  207. }
  208. test('expression statement', () => {
  209. assertAwaitDetection(`await foo`)
  210. })
  211. test('variable', () => {
  212. assertAwaitDetection(`const a = 1 + (await foo)`)
  213. })
  214. test('ref', () => {
  215. assertAwaitDetection(`ref: a = 1 + (await foo)`)
  216. })
  217. test('nested statements', () => {
  218. assertAwaitDetection(`if (ok) { await foo } else { await bar }`)
  219. })
  220. test('should ignore await inside functions', () => {
  221. // function declaration
  222. assertAwaitDetection(`async function foo() { await bar }`, false)
  223. // function expression
  224. assertAwaitDetection(`const foo = async () => { await bar }`, false)
  225. // object method
  226. assertAwaitDetection(`const obj = { async method() { await bar }}`, false)
  227. // class method
  228. assertAwaitDetection(
  229. `const cls = class Foo { async method() { await bar }}`,
  230. false
  231. )
  232. })
  233. })
  234. describe('ref: syntax sugar', () => {
  235. test('convert ref declarations', () => {
  236. const { content, bindings } = compile(`<script setup>
  237. ref: a = 1
  238. ref: b = {
  239. count: 0
  240. }
  241. let c = () => {}
  242. let d
  243. </script>`)
  244. expect(content).toMatch(`import { ref } from 'vue'`)
  245. expect(content).not.toMatch(`ref: a`)
  246. expect(content).toMatch(`const a = ref(1)`)
  247. expect(content).toMatch(`
  248. const b = ref({
  249. count: 0
  250. })
  251. `)
  252. // normal declarations left untouched
  253. expect(content).toMatch(`let c = () => {}`)
  254. expect(content).toMatch(`let d`)
  255. assertCode(content)
  256. expect(bindings).toStrictEqual({
  257. a: 'setup',
  258. b: 'setup',
  259. c: 'setup',
  260. d: 'setup'
  261. })
  262. })
  263. test('multi ref declarations', () => {
  264. const { content, bindings } = compile(`<script setup>
  265. ref: a = 1, b = 2, c = {
  266. count: 0
  267. }
  268. </script>`)
  269. expect(content).toMatch(`
  270. const a = ref(1), b = ref(2), c = ref({
  271. count: 0
  272. })
  273. `)
  274. expect(content).toMatch(`return { a, b, c }`)
  275. assertCode(content)
  276. expect(bindings).toStrictEqual({
  277. a: 'setup',
  278. b: 'setup',
  279. c: 'setup'
  280. })
  281. })
  282. test('should not convert non ref labels', () => {
  283. const { content } = compile(`<script setup>
  284. foo: a = 1, b = 2, c = {
  285. count: 0
  286. }
  287. </script>`)
  288. expect(content).toMatch(`foo: a = 1, b = 2`)
  289. assertCode(content)
  290. })
  291. test('accessing ref binding', () => {
  292. const { content } = compile(`<script setup>
  293. ref: a = 1
  294. console.log(a)
  295. function get() {
  296. return a + 1
  297. }
  298. </script>`)
  299. expect(content).toMatch(`console.log(a.value)`)
  300. expect(content).toMatch(`return a.value + 1`)
  301. assertCode(content)
  302. })
  303. test('cases that should not append .value', () => {
  304. const { content } = compile(`<script setup>
  305. ref: a = 1
  306. console.log(b.a)
  307. function get(a) {
  308. return a + 1
  309. }
  310. </script>`)
  311. expect(content).not.toMatch(`a.value`)
  312. })
  313. test('mutating ref binding', () => {
  314. const { content } = compile(`<script setup>
  315. ref: a = 1
  316. ref: b = { count: 0 }
  317. function inc() {
  318. a++
  319. a = a + 1
  320. b.count++
  321. b.count = b.count + 1
  322. }
  323. </script>`)
  324. expect(content).toMatch(`a.value++`)
  325. expect(content).toMatch(`a.value = a.value + 1`)
  326. expect(content).toMatch(`b.value.count++`)
  327. expect(content).toMatch(`b.value.count = b.value.count + 1`)
  328. assertCode(content)
  329. })
  330. test('using ref binding in property shorthand', () => {
  331. const { content } = compile(`<script setup>
  332. ref: a = 1
  333. const b = { a }
  334. function test() {
  335. const { a } = b
  336. }
  337. </script>`)
  338. expect(content).toMatch(`const b = { a: a.value }`)
  339. // should not convert destructure
  340. expect(content).toMatch(`const { a } = b`)
  341. assertCode(content)
  342. })
  343. test('object destructure', () => {
  344. const { content, bindings } = compile(`<script setup>
  345. ref: n = 1, ({ a, b: c, d = 1, e: f = 2, ...g } = useFoo())
  346. console.log(n, a, c, d, f, g)
  347. </script>`)
  348. expect(content).toMatch(
  349. `const n = ref(1), { a: __a, b: __c, d: __d = 1, e: __f = 2, ...__g } = useFoo()`
  350. )
  351. expect(content).toMatch(`\nconst a = ref(__a);`)
  352. expect(content).not.toMatch(`\nconst b = ref(__b);`)
  353. expect(content).toMatch(`\nconst c = ref(__c);`)
  354. expect(content).toMatch(`\nconst d = ref(__d);`)
  355. expect(content).not.toMatch(`\nconst e = ref(__e);`)
  356. expect(content).toMatch(`\nconst f = ref(__f);`)
  357. expect(content).toMatch(`\nconst g = ref(__g);`)
  358. expect(content).toMatch(
  359. `console.log(n.value, a.value, c.value, d.value, f.value, g.value)`
  360. )
  361. expect(content).toMatch(`return { n, a, c, d, f, g }`)
  362. expect(bindings).toStrictEqual({
  363. n: 'setup',
  364. a: 'setup',
  365. c: 'setup',
  366. d: 'setup',
  367. f: 'setup',
  368. g: 'setup'
  369. })
  370. assertCode(content)
  371. })
  372. test('array destructure', () => {
  373. const { content, bindings } = compile(`<script setup>
  374. ref: n = 1, [a, b = 1, ...c] = useFoo()
  375. console.log(n, a, b, c)
  376. </script>`)
  377. expect(content).toMatch(
  378. `const n = ref(1), [__a, __b = 1, ...__c] = useFoo()`
  379. )
  380. expect(content).toMatch(`\nconst a = ref(__a);`)
  381. expect(content).toMatch(`\nconst b = ref(__b);`)
  382. expect(content).toMatch(`\nconst c = ref(__c);`)
  383. expect(content).toMatch(`console.log(n.value, a.value, b.value, c.value)`)
  384. expect(content).toMatch(`return { n, a, b, c }`)
  385. expect(bindings).toStrictEqual({
  386. n: 'setup',
  387. a: 'setup',
  388. b: 'setup',
  389. c: 'setup'
  390. })
  391. assertCode(content)
  392. })
  393. test('nested destructure', () => {
  394. const { content, bindings } = compile(`<script setup>
  395. ref: [{ a: { b }}] = useFoo()
  396. ref: ({ c: [d, e] } = useBar())
  397. console.log(b, d, e)
  398. </script>`)
  399. expect(content).toMatch(`const [{ a: { b: __b }}] = useFoo()`)
  400. expect(content).toMatch(`const { c: [__d, __e] } = useBar()`)
  401. expect(content).not.toMatch(`\nconst a = ref(__a);`)
  402. expect(content).not.toMatch(`\nconst c = ref(__c);`)
  403. expect(content).toMatch(`\nconst b = ref(__b);`)
  404. expect(content).toMatch(`\nconst d = ref(__d);`)
  405. expect(content).toMatch(`\nconst e = ref(__e);`)
  406. expect(content).toMatch(`return { b, d, e }`)
  407. expect(bindings).toStrictEqual({
  408. b: 'setup',
  409. d: 'setup',
  410. e: 'setup'
  411. })
  412. assertCode(content)
  413. })
  414. })
  415. describe('errors', () => {
  416. test('<script> and <script setup> must have same lang', () => {
  417. expect(() =>
  418. compile(`<script>foo()</script><script setup lang="ts">bar()</script>`)
  419. ).toThrow(`<script> and <script setup> must have the same language type`)
  420. })
  421. test('non-type named exports', () => {
  422. expect(() =>
  423. compile(`<script setup>
  424. export const a = 1
  425. </script>`)
  426. ).toThrow(`cannot contain non-type named exports`)
  427. expect(() =>
  428. compile(`<script setup>
  429. const bar = 1
  430. export { bar as default }
  431. </script>`)
  432. ).toThrow(`cannot contain non-type named exports`)
  433. })
  434. test('ref: non-assignment expressions', () => {
  435. expect(() =>
  436. compile(`<script setup>
  437. ref: a = 1, foo()
  438. </script>`)
  439. ).toThrow(`ref: statements can only contain assignment expressions`)
  440. })
  441. test('export default referencing local var', () => {
  442. expect(() =>
  443. compile(`<script setup>
  444. const bar = 1
  445. export default {
  446. props: {
  447. foo: {
  448. default: () => bar
  449. }
  450. }
  451. }
  452. </script>`)
  453. ).toThrow(`cannot reference locally declared variables`)
  454. })
  455. test('export default referencing ref declarations', () => {
  456. expect(() =>
  457. compile(`<script setup>
  458. ref: bar = 1
  459. export default {
  460. props: bar
  461. }
  462. </script>`)
  463. ).toThrow(`cannot reference locally declared variables`)
  464. })
  465. test('should allow export default referencing scope var', () => {
  466. assertCode(
  467. compile(`<script setup>
  468. const bar = 1
  469. export default {
  470. props: {
  471. foo: {
  472. default: bar => bar + 1
  473. }
  474. }
  475. }
  476. </script>`).content
  477. )
  478. })
  479. test('should allow export default referencing imported binding', () => {
  480. assertCode(
  481. compile(`<script setup>
  482. import { bar } from './bar'
  483. export default {
  484. props: {
  485. foo: {
  486. default: () => bar
  487. }
  488. }
  489. }
  490. </script>`).content
  491. )
  492. })
  493. test('error on duplicated default export', () => {
  494. expect(() =>
  495. compile(`
  496. <script>
  497. export default {}
  498. </script>
  499. <script setup>
  500. export default {}
  501. </script>
  502. `)
  503. ).toThrow(`Default export is already declared`)
  504. expect(() =>
  505. compile(`
  506. <script>
  507. export { x as default } from './y'
  508. </script>
  509. <script setup>
  510. export default {}
  511. </script>
  512. `)
  513. ).toThrow(`Default export is already declared`)
  514. expect(() =>
  515. compile(`
  516. <script>
  517. const x = {}
  518. export { x as default }
  519. </script>
  520. <script setup>
  521. export default {}
  522. </script>
  523. `)
  524. ).toThrow(`Default export is already declared`)
  525. })
  526. })
  527. })
  528. describe('SFC analyze <script> bindings', () => {
  529. it('can parse decorators syntax in typescript block', () => {
  530. const { scriptAst } = compile(`
  531. <script lang="ts">
  532. import { Options, Vue } from 'vue-class-component';
  533. @Options({
  534. components: {
  535. HelloWorld,
  536. },
  537. props: ['foo', 'bar']
  538. })
  539. export default class Home extends Vue {}
  540. </script>
  541. `)
  542. expect(scriptAst).toBeDefined()
  543. })
  544. it('recognizes props array declaration', () => {
  545. const { bindings } = compile(`
  546. <script>
  547. export default {
  548. props: ['foo', 'bar']
  549. }
  550. </script>
  551. `)
  552. expect(bindings).toStrictEqual({ foo: 'props', bar: 'props' })
  553. })
  554. it('recognizes props object declaration', () => {
  555. const { bindings } = compile(`
  556. <script>
  557. export default {
  558. props: {
  559. foo: String,
  560. bar: {
  561. type: String,
  562. },
  563. baz: null,
  564. qux: [String, Number]
  565. }
  566. }
  567. </script>
  568. `)
  569. expect(bindings).toStrictEqual({
  570. foo: 'props',
  571. bar: 'props',
  572. baz: 'props',
  573. qux: 'props'
  574. })
  575. })
  576. it('recognizes setup return', () => {
  577. const { bindings } = compile(`
  578. <script>
  579. const bar = 2
  580. export default {
  581. setup() {
  582. return {
  583. foo: 1,
  584. bar
  585. }
  586. }
  587. }
  588. </script>
  589. `)
  590. expect(bindings).toStrictEqual({ foo: 'setup', bar: 'setup' })
  591. })
  592. it('recognizes async setup return', () => {
  593. const { bindings } = compile(`
  594. <script>
  595. const bar = 2
  596. export default {
  597. async setup() {
  598. return {
  599. foo: 1,
  600. bar
  601. }
  602. }
  603. }
  604. </script>
  605. `)
  606. expect(bindings).toStrictEqual({ foo: 'setup', bar: 'setup' })
  607. })
  608. it('recognizes data return', () => {
  609. const { bindings } = compile(`
  610. <script>
  611. const bar = 2
  612. export default {
  613. data() {
  614. return {
  615. foo: null,
  616. bar
  617. }
  618. }
  619. }
  620. </script>
  621. `)
  622. expect(bindings).toStrictEqual({ foo: 'data', bar: 'data' })
  623. })
  624. it('recognizes methods', () => {
  625. const { bindings } = compile(`
  626. <script>
  627. export default {
  628. methods: {
  629. foo() {}
  630. }
  631. }
  632. </script>
  633. `)
  634. expect(bindings).toStrictEqual({ foo: 'options' })
  635. })
  636. it('recognizes computeds', () => {
  637. const { bindings } = compile(`
  638. <script>
  639. export default {
  640. computed: {
  641. foo() {},
  642. bar: {
  643. get() {},
  644. set() {},
  645. }
  646. }
  647. }
  648. </script>
  649. `)
  650. expect(bindings).toStrictEqual({ foo: 'options', bar: 'options' })
  651. })
  652. it('recognizes injections array declaration', () => {
  653. const { bindings } = compile(`
  654. <script>
  655. export default {
  656. inject: ['foo', 'bar']
  657. }
  658. </script>
  659. `)
  660. expect(bindings).toStrictEqual({ foo: 'options', bar: 'options' })
  661. })
  662. it('recognizes injections object declaration', () => {
  663. const { bindings } = compile(`
  664. <script>
  665. export default {
  666. inject: {
  667. foo: {},
  668. bar: {},
  669. }
  670. }
  671. </script>
  672. `)
  673. expect(bindings).toStrictEqual({ foo: 'options', bar: 'options' })
  674. })
  675. it('works for mixed bindings', () => {
  676. const { bindings } = compile(`
  677. <script>
  678. export default {
  679. inject: ['foo'],
  680. props: {
  681. bar: String,
  682. },
  683. setup() {
  684. return {
  685. baz: null,
  686. }
  687. },
  688. data() {
  689. return {
  690. qux: null
  691. }
  692. },
  693. methods: {
  694. quux() {}
  695. },
  696. computed: {
  697. quuz() {}
  698. }
  699. }
  700. </script>
  701. `)
  702. expect(bindings).toStrictEqual({
  703. foo: 'options',
  704. bar: 'props',
  705. baz: 'setup',
  706. qux: 'data',
  707. quux: 'options',
  708. quuz: 'options'
  709. })
  710. })
  711. it('works for script setup', () => {
  712. const { bindings } = compile(`
  713. <script setup>
  714. export default {
  715. props: {
  716. foo: String,
  717. },
  718. }
  719. </script>
  720. `)
  721. expect(bindings).toStrictEqual({
  722. foo: 'props'
  723. })
  724. })
  725. })