compileScript.spec.ts 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519
  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('CSS vars injection', () => {
  269. test('<script> w/ no default export', () => {
  270. assertCode(
  271. compile(
  272. `<script>const a = 1</script>\n` +
  273. `<style vars="{ color }">div{ color: var(--color); }</style>`
  274. ).content
  275. )
  276. })
  277. test('<script> w/ default export', () => {
  278. assertCode(
  279. compile(
  280. `<script>export default { setup() {} }</script>\n` +
  281. `<style vars="{ color }">div{ color: var(--color); }</style>`
  282. ).content
  283. )
  284. })
  285. test('<script> w/ default export in strings/comments', () => {
  286. assertCode(
  287. compile(
  288. `<script>
  289. // export default {}
  290. export default {}
  291. </script>\n` +
  292. `<style vars="{ color }">div{ color: var(--color); }</style>`
  293. ).content
  294. )
  295. })
  296. test('w/ <script setup>', () => {
  297. assertCode(
  298. compile(
  299. `<script setup>export const color = 'red'</script>\n` +
  300. `<style vars="{ color }">div{ color: var(--color); }</style>`
  301. ).content
  302. )
  303. })
  304. })
  305. describe('async/await detection', () => {
  306. function assertAwaitDetection(code: string, shouldAsync = true) {
  307. const { content } = compile(`<script setup>${code}</script>`)
  308. expect(content).toMatch(
  309. `export ${shouldAsync ? `async ` : ``}function setup`
  310. )
  311. }
  312. test('expression statement', () => {
  313. assertAwaitDetection(`await foo`)
  314. })
  315. test('variable', () => {
  316. assertAwaitDetection(`const a = 1 + (await foo)`)
  317. })
  318. test('export', () => {
  319. assertAwaitDetection(`export const a = 1 + (await foo)`)
  320. })
  321. test('nested statements', () => {
  322. assertAwaitDetection(`if (ok) { await foo } else { await bar }`)
  323. })
  324. test('should ignore await inside functions', () => {
  325. // function declaration
  326. assertAwaitDetection(`export async function foo() { await bar }`, false)
  327. // function expression
  328. assertAwaitDetection(`const foo = async () => { await bar }`, false)
  329. // object method
  330. assertAwaitDetection(`const obj = { async method() { await bar }}`, false)
  331. // class method
  332. assertAwaitDetection(
  333. `const cls = class Foo { async method() { await bar }}`,
  334. false
  335. )
  336. })
  337. })
  338. describe('errors', () => {
  339. test('<script> and <script setup> must have same lang', () => {
  340. expect(
  341. parse(`<script>foo()</script><script setup lang="ts">bar()</script>`)
  342. .errors[0].message
  343. ).toMatch(`<script> and <script setup> must have the same language type`)
  344. })
  345. test('export local as default', () => {
  346. expect(
  347. parse(`<script setup>
  348. const bar = 1
  349. export { bar as default }
  350. </script>`).errors[0].message
  351. ).toMatch(`Cannot export locally defined variable as default`)
  352. })
  353. test('export default referencing local var', () => {
  354. expect(
  355. parse(`<script setup>
  356. const bar = 1
  357. export default {
  358. props: {
  359. foo: {
  360. default: () => bar
  361. }
  362. }
  363. }
  364. </script>`).errors[0].message
  365. ).toMatch(`cannot reference locally declared variables`)
  366. })
  367. test('export default referencing exports', () => {
  368. expect(
  369. parse(`<script setup>
  370. export const bar = 1
  371. export default {
  372. props: bar
  373. }
  374. </script>`).errors[0].message
  375. ).toMatch(`cannot reference locally declared variables`)
  376. })
  377. test('should allow export default referencing scope var', () => {
  378. assertCode(
  379. compile(`<script setup>
  380. const bar = 1
  381. export default {
  382. props: {
  383. foo: {
  384. default: bar => bar + 1
  385. }
  386. }
  387. }
  388. </script>`).content
  389. )
  390. })
  391. test('should allow export default referencing imported binding', () => {
  392. assertCode(
  393. compile(`<script setup>
  394. import { bar } from './bar'
  395. export { bar }
  396. export default {
  397. props: {
  398. foo: {
  399. default: () => bar
  400. }
  401. }
  402. }
  403. </script>`).content
  404. )
  405. })
  406. test('should allow export default referencing re-exported binding', () => {
  407. assertCode(
  408. compile(`<script setup>
  409. export { bar } from './bar'
  410. export default {
  411. props: {
  412. foo: {
  413. default: () => bar
  414. }
  415. }
  416. }
  417. </script>`).content
  418. )
  419. })
  420. test('error on duplicated defalut export', () => {
  421. expect(
  422. parse(`
  423. <script>
  424. export default {}
  425. </script>
  426. <script setup>
  427. export default {}
  428. </script>
  429. `).errors[0].message
  430. ).toMatch(`Default export is already declared`)
  431. expect(
  432. parse(`
  433. <script>
  434. export default {}
  435. </script>
  436. <script setup>
  437. const x = {}
  438. export { x as default }
  439. </script>
  440. `).errors[0].message
  441. ).toMatch(`Default export is already declared`)
  442. expect(
  443. parse(`
  444. <script>
  445. export default {}
  446. </script>
  447. <script setup>
  448. export { x as default } from './y'
  449. </script>
  450. `).errors[0].message
  451. ).toMatch(`Default export is already declared`)
  452. expect(
  453. parse(`
  454. <script>
  455. export { x as default } from './y'
  456. </script>
  457. <script setup>
  458. export default {}
  459. </script>
  460. `).errors[0].message
  461. ).toMatch(`Default export is already declared`)
  462. expect(
  463. parse(`
  464. <script>
  465. const x = {}
  466. export { x as default }
  467. </script>
  468. <script setup>
  469. export default {}
  470. </script>
  471. `).errors[0].message
  472. ).toMatch(`Default export is already declared`)
  473. })
  474. })
  475. })