parse.spec.ts 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450
  1. import { parse } from '../src'
  2. import {
  3. ElementTypes,
  4. NodeTypes,
  5. baseCompile,
  6. createRoot,
  7. } from '@vue/compiler-core'
  8. import { SourceMapConsumer } from 'source-map-js'
  9. describe('compiler:sfc', () => {
  10. describe('source map', () => {
  11. test('style block', () => {
  12. // Padding determines how many blank lines will there be before the style block
  13. const padding = Math.round(Math.random() * 10)
  14. const src =
  15. `${'\n'.repeat(padding)}` +
  16. `<style>
  17. .css {
  18. color: red;
  19. }
  20. </style>
  21. <style module>
  22. .css-module {
  23. color: red;
  24. }
  25. </style>
  26. <style scoped>
  27. .css-scoped {
  28. color: red;
  29. }
  30. </style>
  31. <style scoped>
  32. .css-scoped-nested {
  33. color: red;
  34. .dummy {
  35. color: green;
  36. }
  37. font-weight: bold;
  38. }
  39. </style>`
  40. const {
  41. descriptor: { styles },
  42. } = parse(src)
  43. expect(styles[0].map).not.toBeUndefined()
  44. const consumer = new SourceMapConsumer(styles[0].map!)
  45. const lineOffset =
  46. src.slice(0, src.indexOf(`<style>`)).split('\n').length - 1
  47. consumer.eachMapping(mapping => {
  48. expect(mapping.generatedLine + lineOffset).toBe(mapping.originalLine)
  49. })
  50. expect(styles[1].map).not.toBeUndefined()
  51. const consumer1 = new SourceMapConsumer(styles[1].map!)
  52. const lineOffset1 =
  53. src.slice(0, src.indexOf(`<style module>`)).split('\n').length - 1
  54. consumer1.eachMapping(mapping => {
  55. expect(mapping.generatedLine + lineOffset1).toBe(mapping.originalLine)
  56. })
  57. expect(styles[2].map).not.toBeUndefined()
  58. const consumer2 = new SourceMapConsumer(styles[2].map!)
  59. const lineOffset2 =
  60. src.slice(0, src.indexOf(`<style scoped>`)).split('\n').length - 1
  61. consumer2.eachMapping(mapping => {
  62. expect(mapping.generatedLine + lineOffset2).toBe(mapping.originalLine)
  63. })
  64. })
  65. test('script block', () => {
  66. // Padding determines how many blank lines will there be before the style block
  67. const padding = Math.round(Math.random() * 10)
  68. const script = parse(
  69. `${'\n'.repeat(padding)}<script>\nconsole.log(1)\n }\n</script>\n`,
  70. ).descriptor.script
  71. expect(script!.map).not.toBeUndefined()
  72. const consumer = new SourceMapConsumer(script!.map!)
  73. consumer.eachMapping(mapping => {
  74. expect(mapping.originalLine! - mapping.generatedLine).toBe(padding)
  75. })
  76. })
  77. test('template block with lang + indent', () => {
  78. // Padding determines how many blank lines will there be before the style block
  79. const padding = Math.round(Math.random() * 10)
  80. const template = parse(
  81. `${'\n'.repeat(padding)}<template lang="pug">
  82. h1 foo
  83. div bar
  84. span baz
  85. </template>\n`,
  86. ).descriptor.template!
  87. expect(template.map).not.toBeUndefined()
  88. const consumer = new SourceMapConsumer(template.map!)
  89. consumer.eachMapping(mapping => {
  90. expect(mapping.originalLine! - mapping.generatedLine).toBe(padding)
  91. expect(mapping.originalColumn! - mapping.generatedColumn).toBe(2)
  92. })
  93. })
  94. test('custom block', () => {
  95. const padding = Math.round(Math.random() * 10)
  96. const custom = parse(
  97. `${'\n'.repeat(padding)}<i18n>\n{\n "greeting": "hello"\n}\n</i18n>\n`,
  98. ).descriptor.customBlocks[0]
  99. expect(custom!.map).not.toBeUndefined()
  100. const consumer = new SourceMapConsumer(custom!.map!)
  101. consumer.eachMapping(mapping => {
  102. expect(mapping.originalLine! - mapping.generatedLine).toBe(padding)
  103. })
  104. })
  105. })
  106. test('pad content', () => {
  107. const content = `
  108. <template>
  109. <div></div>
  110. </template>
  111. <script>
  112. export default {}
  113. </script>
  114. <style>
  115. h1 { color: red }
  116. </style>
  117. <i18n>
  118. { "greeting": "hello" }
  119. </i18n>
  120. `
  121. const padFalse = parse(content.trim(), { pad: false }).descriptor
  122. expect(padFalse.template!.content).toBe('\n<div></div>\n')
  123. expect(padFalse.script!.content).toBe('\nexport default {}\n')
  124. expect(padFalse.styles[0].content).toBe('\nh1 { color: red }\n')
  125. expect(padFalse.customBlocks[0].content).toBe('\n{ "greeting": "hello" }\n')
  126. const padTrue = parse(content.trim(), { pad: true }).descriptor
  127. expect(padTrue.script!.content).toBe(
  128. Array(3 + 1).join('//\n') + '\nexport default {}\n',
  129. )
  130. expect(padTrue.styles[0].content).toBe(
  131. Array(6 + 1).join('\n') + '\nh1 { color: red }\n',
  132. )
  133. expect(padTrue.customBlocks[0].content).toBe(
  134. Array(9 + 1).join('\n') + '\n{ "greeting": "hello" }\n',
  135. )
  136. const padLine = parse(content.trim(), { pad: 'line' }).descriptor
  137. expect(padLine.script!.content).toBe(
  138. Array(3 + 1).join('//\n') + '\nexport default {}\n',
  139. )
  140. expect(padLine.styles[0].content).toBe(
  141. Array(6 + 1).join('\n') + '\nh1 { color: red }\n',
  142. )
  143. expect(padLine.customBlocks[0].content).toBe(
  144. Array(9 + 1).join('\n') + '\n{ "greeting": "hello" }\n',
  145. )
  146. const padSpace = parse(content.trim(), { pad: 'space' }).descriptor
  147. expect(padSpace.script!.content).toBe(
  148. `<template>\n<div></div>\n</template>\n<script>`.replace(/./g, ' ') +
  149. '\nexport default {}\n',
  150. )
  151. expect(padSpace.styles[0].content).toBe(
  152. `<template>\n<div></div>\n</template>\n<script>\nexport default {}\n</script>\n<style>`.replace(
  153. /./g,
  154. ' ',
  155. ) + '\nh1 { color: red }\n',
  156. )
  157. expect(padSpace.customBlocks[0].content).toBe(
  158. `<template>\n<div></div>\n</template>\n<script>\nexport default {}\n</script>\n<style>\nh1 { color: red }\n</style>\n<i18n>`.replace(
  159. /./g,
  160. ' ',
  161. ) + '\n{ "greeting": "hello" }\n',
  162. )
  163. })
  164. test('should parse correct range for root level self closing tag', () => {
  165. const content = `\n <div/>\n`
  166. const { descriptor } = parse(`<template>${content}</template>`)
  167. expect(descriptor.template).toBeTruthy()
  168. expect(descriptor.template!.content).toBe(content)
  169. expect(descriptor.template!.loc).toMatchObject({
  170. start: { line: 1, column: 11, offset: 10 },
  171. end: {
  172. line: 3,
  173. column: 1,
  174. offset: 10 + content.length,
  175. },
  176. })
  177. })
  178. test('should parse correct range for blocks with no content (self closing)', () => {
  179. const { descriptor } = parse(`<template/>`)
  180. expect(descriptor.template).toBeTruthy()
  181. expect(descriptor.template!.content).toBeFalsy()
  182. expect(descriptor.template!.loc).toMatchObject({
  183. start: { line: 1, column: 12, offset: 11 },
  184. end: { line: 1, column: 12, offset: 11 },
  185. })
  186. })
  187. test('should parse correct range for blocks with no content (explicit)', () => {
  188. const { descriptor } = parse(`<template></template>`)
  189. expect(descriptor.template).toBeTruthy()
  190. expect(descriptor.template!.content).toBeFalsy()
  191. expect(descriptor.template!.loc).toMatchObject({
  192. start: { line: 1, column: 11, offset: 10 },
  193. end: { line: 1, column: 11, offset: 10 },
  194. })
  195. })
  196. test('should ignore other nodes with no content', () => {
  197. expect(parse(`<script/>`).descriptor.script).toBe(null)
  198. expect(parse(`<script> \n\t </script>`).descriptor.script).toBe(null)
  199. expect(parse(`<style/>`).descriptor.styles.length).toBe(0)
  200. expect(parse(`<style> \n\t </style>`).descriptor.styles.length).toBe(0)
  201. expect(parse(`<custom/>`).descriptor.customBlocks.length).toBe(0)
  202. expect(
  203. parse(`<custom> \n\t </custom>`).descriptor.customBlocks.length,
  204. ).toBe(0)
  205. })
  206. test('handle empty nodes with src attribute', () => {
  207. const { descriptor } = parse(`<script src="com"/>`)
  208. expect(descriptor.script).toBeTruthy()
  209. expect(descriptor.script!.content).toBeFalsy()
  210. expect(descriptor.script!.attrs['src']).toBe('com')
  211. })
  212. test('should not expose ast on template node if has src import', () => {
  213. const { descriptor } = parse(`<template src="./foo.html"/>`)
  214. expect(descriptor.template!.ast).toBeUndefined()
  215. })
  216. test('ignoreEmpty: false', () => {
  217. const { descriptor } = parse(
  218. `<script></script>\n<script setup>\n</script>`,
  219. {
  220. ignoreEmpty: false,
  221. },
  222. )
  223. expect(descriptor.script).toBeTruthy()
  224. expect(descriptor.script!.loc).toMatchObject({
  225. start: { line: 1, column: 9, offset: 8 },
  226. end: { line: 1, column: 9, offset: 8 },
  227. })
  228. expect(descriptor.scriptSetup).toBeTruthy()
  229. expect(descriptor.scriptSetup!.loc).toMatchObject({
  230. start: { line: 2, column: 15, offset: 32 },
  231. end: { line: 3, column: 1, offset: 33 },
  232. })
  233. })
  234. test('nested templates', () => {
  235. const content = `
  236. <template v-if="ok">ok</template>
  237. <div><div></div></div>
  238. `
  239. const { descriptor } = parse(`<template>${content}</template>`)
  240. expect(descriptor.template!.content).toBe(content)
  241. })
  242. test('treat empty lang attribute as the html', () => {
  243. const content = `<div><template v-if="ok">ok</template></div>`
  244. const { descriptor, errors } = parse(
  245. `<template lang="">${content}</template>`,
  246. )
  247. expect(descriptor.template!.content).toBe(content)
  248. expect(errors.length).toBe(0)
  249. })
  250. // #1120
  251. test('template with preprocessor lang should be treated as plain text', () => {
  252. const content = `p(v-if="1 < 2") test <div/>`
  253. const { descriptor, errors } = parse(
  254. `<template lang="pug">` + content + `</template>`,
  255. )
  256. expect(errors.length).toBe(0)
  257. expect(descriptor.template!.content).toBe(content)
  258. // should not attempt to parse the content
  259. expect(descriptor.template!.ast!.children.length).toBe(1)
  260. })
  261. //#2566
  262. test('div lang should not be treated as plain text', () => {
  263. const { errors } = parse(`
  264. <template lang="pug">
  265. <div lang="">
  266. <div></div>
  267. </div>
  268. </template>
  269. `)
  270. expect(errors.length).toBe(0)
  271. })
  272. test('slotted detection', async () => {
  273. expect(parse(`<template>hi</template>`).descriptor.slotted).toBe(false)
  274. expect(
  275. parse(`<template>hi</template><style>h1{color:red;}</style>`).descriptor
  276. .slotted,
  277. ).toBe(false)
  278. expect(
  279. parse(
  280. `<template>hi</template><style scoped>:slotted(h1){color:red;}</style>`,
  281. ).descriptor.slotted,
  282. ).toBe(true)
  283. expect(
  284. parse(
  285. `<template>hi</template><style scoped>::v-slotted(h1){color:red;}</style>`,
  286. ).descriptor.slotted,
  287. ).toBe(true)
  288. })
  289. test('error tolerance', () => {
  290. const { errors } = parse(`<template>`)
  291. expect(errors.length).toBe(1)
  292. })
  293. test('should parse as DOM by default', () => {
  294. const { errors } = parse(`<template><input></template>`)
  295. expect(errors.length).toBe(0)
  296. })
  297. test('custom compiler', () => {
  298. const { errors } = parse(`<template><input></template>`, {
  299. compiler: {
  300. parse: (_, options) => {
  301. options.onError!(new Error('foo') as any)
  302. return createRoot([])
  303. },
  304. compile: baseCompile,
  305. },
  306. })
  307. expect(errors.length).toBe(2)
  308. // error thrown by the custom parse
  309. expect(errors[0].message).toBe('foo')
  310. // error thrown based on the returned root
  311. expect(errors[1].message).toMatch('At least one')
  312. })
  313. test('treat custom blocks as raw text', () => {
  314. const { errors, descriptor } = parse(
  315. `<template><input></template><foo> <-& </foo>`,
  316. )
  317. expect(errors.length).toBe(0)
  318. expect(descriptor.customBlocks[0].content).toBe(` <-& `)
  319. })
  320. test('should accept parser options', () => {
  321. const { errors, descriptor } = parse(`<template><hello/></template>`, {
  322. templateParseOptions: {
  323. isCustomElement: t => t === 'hello',
  324. },
  325. })
  326. expect(errors.length).toBe(0)
  327. expect(descriptor.template!.ast!.children[0]).toMatchObject({
  328. type: NodeTypes.ELEMENT,
  329. tag: 'hello',
  330. tagType: ElementTypes.ELEMENT,
  331. })
  332. // test cache invalidation on different options
  333. const { descriptor: d2 } = parse(`<template><hello/></template>`, {
  334. templateParseOptions: {
  335. isCustomElement: t => t !== 'hello',
  336. },
  337. })
  338. expect(d2.template!.ast!.children[0]).toMatchObject({
  339. type: NodeTypes.ELEMENT,
  340. tag: 'hello',
  341. tagType: ElementTypes.COMPONENT,
  342. })
  343. })
  344. describe('vapor mode', () => {
  345. test('on empty script', () => {
  346. const { descriptor } = parse(`<script vapor></script>`)
  347. expect(descriptor.vapor).toBe(true)
  348. })
  349. test('on template', () => {
  350. const { descriptor } = parse(`<template vapor><div/></template>`)
  351. expect(descriptor.vapor).toBe(true)
  352. })
  353. })
  354. describe('warnings', () => {
  355. function assertWarning(errors: Error[], msg: string) {
  356. expect(errors.some(e => e.message.match(msg))).toBe(true)
  357. }
  358. test('should only allow single template element', () => {
  359. assertWarning(
  360. parse(`<template><div/></template><template><div/></template>`).errors,
  361. `Single file component can contain only one <template> element`,
  362. )
  363. })
  364. test('should only allow single script element', () => {
  365. assertWarning(
  366. parse(`<script>console.log(1)</script><script>console.log(1)</script>`)
  367. .errors,
  368. `Single file component can contain only one <script> element`,
  369. )
  370. })
  371. test('should only allow single script setup element', () => {
  372. assertWarning(
  373. parse(
  374. `<script setup>console.log(1)</script><script setup>console.log(1)</script>`,
  375. ).errors,
  376. `Single file component can contain only one <script setup> element`,
  377. )
  378. })
  379. test('should not warn script & script setup', () => {
  380. expect(
  381. parse(
  382. `<script setup>console.log(1)</script><script>console.log(1)</script>`,
  383. ).errors.length,
  384. ).toBe(0)
  385. })
  386. // # 6676
  387. test('should throw error if no <template> or <script> is present', () => {
  388. assertWarning(
  389. parse(`import { ref } from 'vue'`).errors,
  390. `At least one <template> or <script> is required in a single file component`,
  391. )
  392. })
  393. test('should throw error if template functional is given', () => {
  394. assertWarning(
  395. parse(`<template functional></template>`).errors,
  396. `<template functional> is no longer supported in Vue 3, since ` +
  397. `functional components no longer have significant performance ` +
  398. `difference from stateful ones. Just use a normal <template> ` +
  399. `instead.`,
  400. )
  401. })
  402. })
  403. })