compileScript.spec.ts 18 KB

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