compileScript.spec.ts 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439
  1. import { parse, SFCScriptCompileOptions } from '../src'
  2. import { parse as babelParse } from '@babel/parser'
  3. import { babelParserDefautPlugins } from '@vue/shared'
  4. function compile(src: string, options?: SFCScriptCompileOptions) {
  5. return parse(src, options).descriptor.scriptTransformed!
  6. }
  7. function assertCode(code: string) {
  8. // parse the generated code to make sure it is valid
  9. try {
  10. babelParse(code, {
  11. sourceType: 'module',
  12. plugins: [...babelParserDefautPlugins, 'typescript']
  13. })
  14. } catch (e) {
  15. console.log(code)
  16. throw e
  17. }
  18. expect(code).toMatchSnapshot()
  19. }
  20. describe('SFC compile <script setup>', () => {
  21. test('should hoist imports', () => {
  22. assertCode(
  23. compile(`<script setup>import { ref } from 'vue'</script>`).content
  24. )
  25. })
  26. test('explicit setup signature', () => {
  27. assertCode(
  28. compile(`<script setup="props, { emit }">emit('foo')</script>`).content
  29. )
  30. })
  31. test('import dedupe between <script> and <script setup>', () => {
  32. const { content } = compile(`
  33. <script>
  34. import { x } from './x'
  35. </script>
  36. <script setup>
  37. import { x } from './x'
  38. x()
  39. </script>
  40. `)
  41. assertCode(content)
  42. expect(content.indexOf(`import { x }`)).toEqual(
  43. content.lastIndexOf(`import { x }`)
  44. )
  45. })
  46. describe('exports', () => {
  47. test('export const x = ...', () => {
  48. const { content, bindings } = compile(
  49. `<script setup>export const x = 1</script>`
  50. )
  51. assertCode(content)
  52. expect(bindings).toStrictEqual({
  53. x: 'setup'
  54. })
  55. })
  56. test('export const { x } = ... (destructuring)', () => {
  57. const { content, bindings } = compile(`<script setup>
  58. export const [a = 1, { b } = { b: 123 }, ...c] = useFoo()
  59. export const { d = 2, _: [e], ...f } = useBar()
  60. </script>`)
  61. assertCode(content)
  62. expect(bindings).toStrictEqual({
  63. a: 'setup',
  64. b: 'setup',
  65. c: 'setup',
  66. d: 'setup',
  67. e: 'setup',
  68. f: 'setup'
  69. })
  70. })
  71. test('export function x() {}', () => {
  72. const { content, bindings } = compile(
  73. `<script setup>export function x(){}</script>`
  74. )
  75. assertCode(content)
  76. expect(bindings).toStrictEqual({
  77. x: 'setup'
  78. })
  79. })
  80. test('export class X() {}', () => {
  81. const { content, bindings } = compile(
  82. `<script setup>export class X {}</script>`
  83. )
  84. assertCode(content)
  85. expect(bindings).toStrictEqual({
  86. X: 'setup'
  87. })
  88. })
  89. test('export { x }', () => {
  90. const { content, bindings } = compile(
  91. `<script setup>
  92. const x = 1
  93. const y = 2
  94. export { x, y }
  95. </script>`
  96. )
  97. assertCode(content)
  98. expect(bindings).toStrictEqual({
  99. x: 'setup',
  100. y: 'setup'
  101. })
  102. })
  103. test(`export { x } from './x'`, () => {
  104. const { content, bindings } = compile(
  105. `<script setup>
  106. export { x, y } from './x'
  107. </script>`
  108. )
  109. assertCode(content)
  110. expect(bindings).toStrictEqual({
  111. x: 'setup',
  112. y: 'setup'
  113. })
  114. })
  115. test(`export default from './x'`, () => {
  116. const { content, bindings } = compile(
  117. `<script setup>
  118. export default from './x'
  119. </script>`,
  120. {
  121. babelParserPlugins: ['exportDefaultFrom']
  122. }
  123. )
  124. assertCode(content)
  125. expect(bindings).toStrictEqual({})
  126. })
  127. test(`export { x as default }`, () => {
  128. const { content, bindings } = compile(
  129. `<script setup>
  130. import x from './x'
  131. const y = 1
  132. export { x as default, y }
  133. </script>`
  134. )
  135. assertCode(content)
  136. expect(bindings).toStrictEqual({
  137. y: 'setup'
  138. })
  139. })
  140. test(`export { x as default } from './x'`, () => {
  141. const { content, bindings } = compile(
  142. `<script setup>
  143. export { x as default, y } from './x'
  144. </script>`
  145. )
  146. assertCode(content)
  147. expect(bindings).toStrictEqual({
  148. y: 'setup'
  149. })
  150. })
  151. test(`export * from './x'`, () => {
  152. const { content, bindings } = compile(
  153. `<script setup>
  154. export * from './x'
  155. export const y = 1
  156. </script>`
  157. )
  158. assertCode(content)
  159. expect(bindings).toStrictEqual({
  160. y: 'setup'
  161. // in this case we cannot extract bindings from ./x so it falls back
  162. // to runtime proxy dispatching
  163. })
  164. })
  165. test('export default in <script setup>', () => {
  166. const { content, bindings } = compile(
  167. `<script setup>
  168. export default {
  169. props: ['foo']
  170. }
  171. export const y = 1
  172. </script>`
  173. )
  174. assertCode(content)
  175. expect(bindings).toStrictEqual({
  176. y: 'setup'
  177. })
  178. })
  179. })
  180. describe('<script setup lang="ts">', () => {
  181. test('hoist type declarations', () => {
  182. const { content, bindings } = compile(`
  183. <script setup lang="ts">
  184. export interface Foo {}
  185. type Bar = {}
  186. export const a = 1
  187. </script>`)
  188. assertCode(content)
  189. expect(bindings).toStrictEqual({ a: 'setup' })
  190. })
  191. test('extract props', () => {
  192. const { content } = compile(`
  193. <script setup="myProps" lang="ts">
  194. interface Test {}
  195. type Alias = number[]
  196. declare const myProps: {
  197. string: string
  198. number: number
  199. boolean: boolean
  200. object: object
  201. objectLiteral: { a: number }
  202. fn: (n: number) => void
  203. functionRef: Function
  204. objectRef: Object
  205. array: string[]
  206. arrayRef: Array<any>
  207. tuple: [number, number]
  208. set: Set<string>
  209. literal: 'foo'
  210. optional?: any
  211. recordRef: Record<string, null>
  212. interface: Test
  213. alias: Alias
  214. union: string | number
  215. literalUnion: 'foo' | 'bar'
  216. literalUnionMixed: 'foo' | 1 | boolean
  217. intersection: Test & {}
  218. }
  219. </script>`)
  220. assertCode(content)
  221. expect(content).toMatch(`string: { type: String, required: true }`)
  222. expect(content).toMatch(`number: { type: Number, required: true }`)
  223. expect(content).toMatch(`boolean: { type: Boolean, required: true }`)
  224. expect(content).toMatch(`object: { type: Object, required: true }`)
  225. expect(content).toMatch(`objectLiteral: { type: Object, required: true }`)
  226. expect(content).toMatch(`fn: { type: Function, required: true }`)
  227. expect(content).toMatch(`functionRef: { type: Function, required: true }`)
  228. expect(content).toMatch(`objectRef: { type: Object, required: true }`)
  229. expect(content).toMatch(`array: { type: Array, required: true }`)
  230. expect(content).toMatch(`arrayRef: { type: Array, required: true }`)
  231. expect(content).toMatch(`tuple: { type: Array, required: true }`)
  232. expect(content).toMatch(`set: { type: Set, required: true }`)
  233. expect(content).toMatch(`literal: { type: String, required: true }`)
  234. expect(content).toMatch(`optional: { type: null, required: false }`)
  235. expect(content).toMatch(`recordRef: { type: Object, required: true }`)
  236. expect(content).toMatch(`interface: { type: Object, required: true }`)
  237. expect(content).toMatch(`alias: { type: Array, required: true }`)
  238. expect(content).toMatch(
  239. `union: { type: [String, Number], required: true }`
  240. )
  241. expect(content).toMatch(
  242. `literalUnion: { type: [String, String], required: true }`
  243. )
  244. expect(content).toMatch(
  245. `literalUnionMixed: { type: [String, Number, Boolean], required: true }`
  246. )
  247. expect(content).toMatch(`intersection: { type: Object, required: true }`)
  248. })
  249. test('extract emits', () => {
  250. const { content } = compile(`
  251. <script setup="_, { emit: myEmit }" lang="ts">
  252. declare function myEmit(e: 'foo' | 'bar'): void
  253. declare function myEmit(e: 'baz', id: number): void
  254. </script>
  255. `)
  256. assertCode(content)
  257. expect(content).toMatch(
  258. `declare function __emit__(e: 'foo' | 'bar'): void`
  259. )
  260. expect(content).toMatch(
  261. `declare function __emit__(e: 'baz', id: number): void`
  262. )
  263. expect(content).toMatch(
  264. `emits: ["foo", "bar", "baz"] as unknown as undefined`
  265. )
  266. })
  267. })
  268. describe('errors', () => {
  269. test('<script> and <script setup> must have same lang', () => {
  270. expect(
  271. parse(`<script>foo()</script><script setup lang="ts">bar()</script>`)
  272. .errors[0].message
  273. ).toMatch(`<script> and <script setup> must have the same language type`)
  274. })
  275. test('export local as default', () => {
  276. expect(
  277. parse(`<script setup>
  278. const bar = 1
  279. export { bar as default }
  280. </script>`).errors[0].message
  281. ).toMatch(`Cannot export locally defined variable as default`)
  282. })
  283. test('export default referencing local var', () => {
  284. expect(
  285. parse(`<script setup>
  286. const bar = 1
  287. export default {
  288. props: {
  289. foo: {
  290. default: () => bar
  291. }
  292. }
  293. }
  294. </script>`).errors[0].message
  295. ).toMatch(`cannot reference locally declared variables`)
  296. })
  297. test('export default referencing exports', () => {
  298. expect(
  299. parse(`<script setup>
  300. export const bar = 1
  301. export default {
  302. props: bar
  303. }
  304. </script>`).errors[0].message
  305. ).toMatch(`cannot reference locally declared variables`)
  306. })
  307. test('should allow export default referencing scope var', () => {
  308. assertCode(
  309. compile(`<script setup>
  310. const bar = 1
  311. export default {
  312. props: {
  313. foo: {
  314. default: bar => bar + 1
  315. }
  316. }
  317. }
  318. </script>`).content
  319. )
  320. })
  321. test('should allow export default referencing imported binding', () => {
  322. assertCode(
  323. compile(`<script setup>
  324. import { bar } from './bar'
  325. export { bar }
  326. export default {
  327. props: {
  328. foo: {
  329. default: () => bar
  330. }
  331. }
  332. }
  333. </script>`).content
  334. )
  335. })
  336. test('should allow export default referencing re-exported binding', () => {
  337. assertCode(
  338. compile(`<script setup>
  339. export { bar } from './bar'
  340. export default {
  341. props: {
  342. foo: {
  343. default: () => bar
  344. }
  345. }
  346. }
  347. </script>`).content
  348. )
  349. })
  350. test('error on duplicated defalut export', () => {
  351. expect(
  352. parse(`
  353. <script>
  354. export default {}
  355. </script>
  356. <script setup>
  357. export default {}
  358. </script>
  359. `).errors[0].message
  360. ).toMatch(`Default export is already declared`)
  361. expect(
  362. parse(`
  363. <script>
  364. export default {}
  365. </script>
  366. <script setup>
  367. const x = {}
  368. export { x as default }
  369. </script>
  370. `).errors[0].message
  371. ).toMatch(`Default export is already declared`)
  372. expect(
  373. parse(`
  374. <script>
  375. export default {}
  376. </script>
  377. <script setup>
  378. export { x as default } from './y'
  379. </script>
  380. `).errors[0].message
  381. ).toMatch(`Default export is already declared`)
  382. expect(
  383. parse(`
  384. <script>
  385. export { x as default } from './y'
  386. </script>
  387. <script setup>
  388. export default {}
  389. </script>
  390. `).errors[0].message
  391. ).toMatch(`Default export is already declared`)
  392. expect(
  393. parse(`
  394. <script>
  395. const x = {}
  396. export { x as default }
  397. </script>
  398. <script setup>
  399. export default {}
  400. </script>
  401. `).errors[0].message
  402. ).toMatch(`Default export is already declared`)
  403. })
  404. })
  405. })