compileScript.spec.ts 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730
  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('can parse decorators syntax in typescript block', () => {
  479. const { scriptAst } = compile(`
  480. <script lang="ts">
  481. import { Options, Vue } from 'vue-class-component';
  482. @Options({
  483. components: {
  484. HelloWorld,
  485. },
  486. props: ['foo', 'bar']
  487. })
  488. export default class Home extends Vue {}
  489. </script>
  490. `)
  491. expect(scriptAst).toBeDefined()
  492. })
  493. it('recognizes props array declaration', () => {
  494. const { bindings } = compile(`
  495. <script>
  496. export default {
  497. props: ['foo', 'bar']
  498. }
  499. </script>
  500. `)
  501. expect(bindings).toStrictEqual({ foo: 'props', bar: 'props' })
  502. })
  503. it('recognizes props object declaration', () => {
  504. const { bindings } = compile(`
  505. <script>
  506. export default {
  507. props: {
  508. foo: String,
  509. bar: {
  510. type: String,
  511. },
  512. baz: null,
  513. qux: [String, Number]
  514. }
  515. }
  516. </script>
  517. `)
  518. expect(bindings).toStrictEqual({
  519. foo: 'props',
  520. bar: 'props',
  521. baz: 'props',
  522. qux: 'props'
  523. })
  524. })
  525. it('recognizes setup return', () => {
  526. const { bindings } = compile(`
  527. <script>
  528. const bar = 2
  529. export default {
  530. setup() {
  531. return {
  532. foo: 1,
  533. bar
  534. }
  535. }
  536. }
  537. </script>
  538. `)
  539. expect(bindings).toStrictEqual({ foo: 'setup', bar: 'setup' })
  540. })
  541. it('recognizes async setup return', () => {
  542. const { bindings } = compile(`
  543. <script>
  544. const bar = 2
  545. export default {
  546. async setup() {
  547. return {
  548. foo: 1,
  549. bar
  550. }
  551. }
  552. }
  553. </script>
  554. `)
  555. expect(bindings).toStrictEqual({ foo: 'setup', bar: 'setup' })
  556. })
  557. it('recognizes data return', () => {
  558. const { bindings } = compile(`
  559. <script>
  560. const bar = 2
  561. export default {
  562. data() {
  563. return {
  564. foo: null,
  565. bar
  566. }
  567. }
  568. }
  569. </script>
  570. `)
  571. expect(bindings).toStrictEqual({ foo: 'data', bar: 'data' })
  572. })
  573. it('recognizes methods', () => {
  574. const { bindings } = compile(`
  575. <script>
  576. export default {
  577. methods: {
  578. foo() {}
  579. }
  580. }
  581. </script>
  582. `)
  583. expect(bindings).toStrictEqual({ foo: 'options' })
  584. })
  585. it('recognizes computeds', () => {
  586. const { bindings } = compile(`
  587. <script>
  588. export default {
  589. computed: {
  590. foo() {},
  591. bar: {
  592. get() {},
  593. set() {},
  594. }
  595. }
  596. }
  597. </script>
  598. `)
  599. expect(bindings).toStrictEqual({ foo: 'options', bar: 'options' })
  600. })
  601. it('recognizes injections array declaration', () => {
  602. const { bindings } = compile(`
  603. <script>
  604. export default {
  605. inject: ['foo', 'bar']
  606. }
  607. </script>
  608. `)
  609. expect(bindings).toStrictEqual({ foo: 'options', bar: 'options' })
  610. })
  611. it('recognizes injections object declaration', () => {
  612. const { bindings } = compile(`
  613. <script>
  614. export default {
  615. inject: {
  616. foo: {},
  617. bar: {},
  618. }
  619. }
  620. </script>
  621. `)
  622. expect(bindings).toStrictEqual({ foo: 'options', bar: 'options' })
  623. })
  624. it('works for mixed bindings', () => {
  625. const { bindings } = compile(`
  626. <script>
  627. export default {
  628. inject: ['foo'],
  629. props: {
  630. bar: String,
  631. },
  632. setup() {
  633. return {
  634. baz: null,
  635. }
  636. },
  637. data() {
  638. return {
  639. qux: null
  640. }
  641. },
  642. methods: {
  643. quux() {}
  644. },
  645. computed: {
  646. quuz() {}
  647. }
  648. }
  649. </script>
  650. `)
  651. expect(bindings).toStrictEqual({
  652. foo: 'options',
  653. bar: 'props',
  654. baz: 'setup',
  655. qux: 'data',
  656. quux: 'options',
  657. quuz: 'options'
  658. })
  659. })
  660. it('works for script setup', () => {
  661. const { bindings } = compile(`
  662. <script setup>
  663. export default {
  664. props: {
  665. foo: String,
  666. },
  667. }
  668. </script>
  669. `)
  670. expect(bindings).toStrictEqual({
  671. foo: 'props'
  672. })
  673. })
  674. })