compileScript.spec.ts 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519
  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. y: 'setup'
  178. })
  179. })
  180. })
  181. describe('<script setup lang="ts">', () => {
  182. test('hoist type declarations', () => {
  183. const { content, bindings } = compile(`
  184. <script setup lang="ts">
  185. export interface Foo {}
  186. type Bar = {}
  187. export const a = 1
  188. </script>`)
  189. assertCode(content)
  190. expect(bindings).toStrictEqual({ a: 'setup' })
  191. })
  192. test('extract props', () => {
  193. const { content } = compile(`
  194. <script setup="myProps" lang="ts">
  195. interface Test {}
  196. type Alias = number[]
  197. declare const myProps: {
  198. string: string
  199. number: number
  200. boolean: boolean
  201. object: object
  202. objectLiteral: { a: number }
  203. fn: (n: number) => void
  204. functionRef: Function
  205. objectRef: Object
  206. array: string[]
  207. arrayRef: Array<any>
  208. tuple: [number, number]
  209. set: Set<string>
  210. literal: 'foo'
  211. optional?: any
  212. recordRef: Record<string, null>
  213. interface: Test
  214. alias: Alias
  215. union: string | number
  216. literalUnion: 'foo' | 'bar'
  217. literalUnionMixed: 'foo' | 1 | boolean
  218. intersection: Test & {}
  219. }
  220. </script>`)
  221. assertCode(content)
  222. expect(content).toMatch(`string: { type: String, required: true }`)
  223. expect(content).toMatch(`number: { type: Number, required: true }`)
  224. expect(content).toMatch(`boolean: { type: Boolean, required: true }`)
  225. expect(content).toMatch(`object: { type: Object, required: true }`)
  226. expect(content).toMatch(`objectLiteral: { type: Object, required: true }`)
  227. expect(content).toMatch(`fn: { type: Function, required: true }`)
  228. expect(content).toMatch(`functionRef: { type: Function, required: true }`)
  229. expect(content).toMatch(`objectRef: { type: Object, required: true }`)
  230. expect(content).toMatch(`array: { type: Array, required: true }`)
  231. expect(content).toMatch(`arrayRef: { type: Array, required: true }`)
  232. expect(content).toMatch(`tuple: { type: Array, required: true }`)
  233. expect(content).toMatch(`set: { type: Set, required: true }`)
  234. expect(content).toMatch(`literal: { type: String, required: true }`)
  235. expect(content).toMatch(`optional: { type: null, required: false }`)
  236. expect(content).toMatch(`recordRef: { type: Object, required: true }`)
  237. expect(content).toMatch(`interface: { type: Object, required: true }`)
  238. expect(content).toMatch(`alias: { type: Array, required: true }`)
  239. expect(content).toMatch(
  240. `union: { type: [String, Number], required: true }`
  241. )
  242. expect(content).toMatch(
  243. `literalUnion: { type: [String, String], required: true }`
  244. )
  245. expect(content).toMatch(
  246. `literalUnionMixed: { type: [String, Number, Boolean], required: true }`
  247. )
  248. expect(content).toMatch(`intersection: { type: Object, required: true }`)
  249. })
  250. test('extract emits', () => {
  251. const { content } = compile(`
  252. <script setup="_, { emit: myEmit }" lang="ts">
  253. declare function myEmit(e: 'foo' | 'bar'): void
  254. declare function myEmit(e: 'baz', id: number): void
  255. </script>
  256. `)
  257. assertCode(content)
  258. expect(content).toMatch(
  259. `declare function __emit__(e: 'foo' | 'bar'): void`
  260. )
  261. expect(content).toMatch(
  262. `declare function __emit__(e: 'baz', id: number): void`
  263. )
  264. expect(content).toMatch(
  265. `emits: ["foo", "bar", "baz"] as unknown as undefined`
  266. )
  267. })
  268. })
  269. describe('CSS vars injection', () => {
  270. test('<script> w/ no default export', () => {
  271. assertCode(
  272. compile(
  273. `<script>const a = 1</script>\n` +
  274. `<style vars="{ color }">div{ color: var(--color); }</style>`
  275. ).content
  276. )
  277. })
  278. test('<script> w/ default export', () => {
  279. assertCode(
  280. compile(
  281. `<script>export default { setup() {} }</script>\n` +
  282. `<style vars="{ color }">div{ color: var(--color); }</style>`
  283. ).content
  284. )
  285. })
  286. test('<script> w/ default export in strings/comments', () => {
  287. assertCode(
  288. compile(
  289. `<script>
  290. // export default {}
  291. export default {}
  292. </script>\n` +
  293. `<style vars="{ color }">div{ color: var(--color); }</style>`
  294. ).content
  295. )
  296. })
  297. test('w/ <script setup>', () => {
  298. assertCode(
  299. compile(
  300. `<script setup>export const color = 'red'</script>\n` +
  301. `<style vars="{ color }">div{ color: var(--color); }</style>`
  302. ).content
  303. )
  304. })
  305. })
  306. describe('async/await detection', () => {
  307. function assertAwaitDetection(code: string, shouldAsync = true) {
  308. const { content } = compile(`<script setup>${code}</script>`)
  309. expect(content).toMatch(
  310. `export ${shouldAsync ? `async ` : ``}function setup`
  311. )
  312. }
  313. test('expression statement', () => {
  314. assertAwaitDetection(`await foo`)
  315. })
  316. test('variable', () => {
  317. assertAwaitDetection(`const a = 1 + (await foo)`)
  318. })
  319. test('export', () => {
  320. assertAwaitDetection(`export const a = 1 + (await foo)`)
  321. })
  322. test('nested statements', () => {
  323. assertAwaitDetection(`if (ok) { await foo } else { await bar }`)
  324. })
  325. test('should ignore await inside functions', () => {
  326. // function declaration
  327. assertAwaitDetection(`export async function foo() { await bar }`, false)
  328. // function expression
  329. assertAwaitDetection(`const foo = async () => { await bar }`, false)
  330. // object method
  331. assertAwaitDetection(`const obj = { async method() { await bar }}`, false)
  332. // class method
  333. assertAwaitDetection(
  334. `const cls = class Foo { async method() { await bar }}`,
  335. false
  336. )
  337. })
  338. })
  339. describe('errors', () => {
  340. test('<script> and <script setup> must have same lang', () => {
  341. expect(() =>
  342. compile(`<script>foo()</script><script setup lang="ts">bar()</script>`)
  343. ).toThrow(`<script> and <script setup> must have the same language type`)
  344. })
  345. test('export local as default', () => {
  346. expect(() =>
  347. compile(`<script setup>
  348. const bar = 1
  349. export { bar as default }
  350. </script>`)
  351. ).toThrow(`Cannot export locally defined variable as default`)
  352. })
  353. test('export default referencing local var', () => {
  354. expect(() =>
  355. compile(`<script setup>
  356. const bar = 1
  357. export default {
  358. props: {
  359. foo: {
  360. default: () => bar
  361. }
  362. }
  363. }
  364. </script>`)
  365. ).toThrow(`cannot reference locally declared variables`)
  366. })
  367. test('export default referencing exports', () => {
  368. expect(() =>
  369. compile(`<script setup>
  370. export const bar = 1
  371. export default {
  372. props: bar
  373. }
  374. </script>`)
  375. ).toThrow(`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 default export', () => {
  421. expect(() =>
  422. compile(`
  423. <script>
  424. export default {}
  425. </script>
  426. <script setup>
  427. export default {}
  428. </script>
  429. `)
  430. ).toThrow(`Default export is already declared`)
  431. expect(() =>
  432. compile(`
  433. <script>
  434. export default {}
  435. </script>
  436. <script setup>
  437. const x = {}
  438. export { x as default }
  439. </script>
  440. `)
  441. ).toThrow(`Default export is already declared`)
  442. expect(() =>
  443. compile(`
  444. <script>
  445. export default {}
  446. </script>
  447. <script setup>
  448. export { x as default } from './y'
  449. </script>
  450. `)
  451. ).toThrow(`Default export is already declared`)
  452. expect(() =>
  453. compile(`
  454. <script>
  455. export { x as default } from './y'
  456. </script>
  457. <script setup>
  458. export default {}
  459. </script>
  460. `)
  461. ).toThrow(`Default export is already declared`)
  462. expect(() =>
  463. compile(`
  464. <script>
  465. const x = {}
  466. export { x as default }
  467. </script>
  468. <script setup>
  469. export default {}
  470. </script>
  471. `)
  472. ).toThrow(`Default export is already declared`)
  473. })
  474. })
  475. })