compileScript.spec.ts 18 KB

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