parse.spec.ts 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343
  1. import { parse } from '../src'
  2. import { baseParse, baseCompile } from '@vue/compiler-core'
  3. import { SourceMapConsumer } from 'source-map-js'
  4. describe('compiler:sfc', () => {
  5. describe('source map', () => {
  6. test('style block', () => {
  7. // Padding determines how many blank lines will there be before the style block
  8. const padding = Math.round(Math.random() * 10)
  9. const style = parse(
  10. `${'\n'.repeat(padding)}<style>\n.color {\n color: red;\n }\n</style>\n`
  11. ).descriptor.styles[0]
  12. expect(style.map).not.toBeUndefined()
  13. const consumer = new SourceMapConsumer(style.map!)
  14. consumer.eachMapping(mapping => {
  15. expect(mapping.originalLine - mapping.generatedLine).toBe(padding)
  16. })
  17. })
  18. test('script block', () => {
  19. // Padding determines how many blank lines will there be before the style block
  20. const padding = Math.round(Math.random() * 10)
  21. const script = parse(
  22. `${'\n'.repeat(padding)}<script>\nconsole.log(1)\n }\n</script>\n`
  23. ).descriptor.script
  24. expect(script!.map).not.toBeUndefined()
  25. const consumer = new SourceMapConsumer(script!.map!)
  26. consumer.eachMapping(mapping => {
  27. expect(mapping.originalLine - mapping.generatedLine).toBe(padding)
  28. })
  29. })
  30. test('template block with lang + indent', () => {
  31. // Padding determines how many blank lines will there be before the style block
  32. const padding = Math.round(Math.random() * 10)
  33. const template = parse(
  34. `${'\n'.repeat(padding)}<template lang="pug">
  35. h1 foo
  36. div bar
  37. span baz
  38. </template>\n`
  39. ).descriptor.template!
  40. expect(template.map).not.toBeUndefined()
  41. const consumer = new SourceMapConsumer(template.map!)
  42. consumer.eachMapping(mapping => {
  43. expect(mapping.originalLine - mapping.generatedLine).toBe(padding)
  44. expect(mapping.originalColumn - mapping.generatedColumn).toBe(2)
  45. })
  46. })
  47. test('custom block', () => {
  48. const padding = Math.round(Math.random() * 10)
  49. const custom = parse(
  50. `${'\n'.repeat(padding)}<i18n>\n{\n "greeting": "hello"\n}\n</i18n>\n`
  51. ).descriptor.customBlocks[0]
  52. expect(custom!.map).not.toBeUndefined()
  53. const consumer = new SourceMapConsumer(custom!.map!)
  54. consumer.eachMapping(mapping => {
  55. expect(mapping.originalLine - mapping.generatedLine).toBe(padding)
  56. })
  57. })
  58. })
  59. test('pad content', () => {
  60. const content = `
  61. <template>
  62. <div></div>
  63. </template>
  64. <script>
  65. export default {}
  66. </script>
  67. <style>
  68. h1 { color: red }
  69. </style>
  70. <i18n>
  71. { "greeting": "hello" }
  72. </i18n>
  73. `
  74. const padFalse = parse(content.trim(), { pad: false }).descriptor
  75. expect(padFalse.template!.content).toBe('\n<div></div>\n')
  76. expect(padFalse.script!.content).toBe('\nexport default {}\n')
  77. expect(padFalse.styles[0].content).toBe('\nh1 { color: red }\n')
  78. expect(padFalse.customBlocks[0].content).toBe('\n{ "greeting": "hello" }\n')
  79. const padTrue = parse(content.trim(), { pad: true }).descriptor
  80. expect(padTrue.script!.content).toBe(
  81. Array(3 + 1).join('//\n') + '\nexport default {}\n'
  82. )
  83. expect(padTrue.styles[0].content).toBe(
  84. Array(6 + 1).join('\n') + '\nh1 { color: red }\n'
  85. )
  86. expect(padTrue.customBlocks[0].content).toBe(
  87. Array(9 + 1).join('\n') + '\n{ "greeting": "hello" }\n'
  88. )
  89. const padLine = parse(content.trim(), { pad: 'line' }).descriptor
  90. expect(padLine.script!.content).toBe(
  91. Array(3 + 1).join('//\n') + '\nexport default {}\n'
  92. )
  93. expect(padLine.styles[0].content).toBe(
  94. Array(6 + 1).join('\n') + '\nh1 { color: red }\n'
  95. )
  96. expect(padLine.customBlocks[0].content).toBe(
  97. Array(9 + 1).join('\n') + '\n{ "greeting": "hello" }\n'
  98. )
  99. const padSpace = parse(content.trim(), { pad: 'space' }).descriptor
  100. expect(padSpace.script!.content).toBe(
  101. `<template>\n<div></div>\n</template>\n<script>`.replace(/./g, ' ') +
  102. '\nexport default {}\n'
  103. )
  104. expect(padSpace.styles[0].content).toBe(
  105. `<template>\n<div></div>\n</template>\n<script>\nexport default {}\n</script>\n<style>`.replace(
  106. /./g,
  107. ' '
  108. ) + '\nh1 { color: red }\n'
  109. )
  110. expect(padSpace.customBlocks[0].content).toBe(
  111. `<template>\n<div></div>\n</template>\n<script>\nexport default {}\n</script>\n<style>\nh1 { color: red }\n</style>\n<i18n>`.replace(
  112. /./g,
  113. ' '
  114. ) + '\n{ "greeting": "hello" }\n'
  115. )
  116. })
  117. test('should parse correct range for root level self closing tag', () => {
  118. const content = `\n <div/>\n`
  119. const { descriptor } = parse(`<template>${content}</template>`)
  120. expect(descriptor.template).toBeTruthy()
  121. expect(descriptor.template!.content).toBe(content)
  122. expect(descriptor.template!.loc).toMatchObject({
  123. start: { line: 1, column: 11, offset: 10 },
  124. end: {
  125. line: 3,
  126. column: 1,
  127. offset: 10 + content.length
  128. },
  129. source: content
  130. })
  131. })
  132. test('should parse correct range for blocks with no content (self closing)', () => {
  133. const { descriptor } = parse(`<template/>`)
  134. expect(descriptor.template).toBeTruthy()
  135. expect(descriptor.template!.content).toBeFalsy()
  136. expect(descriptor.template!.loc).toMatchObject({
  137. start: { line: 1, column: 1, offset: 0 },
  138. end: { line: 1, column: 1, offset: 0 },
  139. source: ''
  140. })
  141. })
  142. test('should parse correct range for blocks with no content (explicit)', () => {
  143. const { descriptor } = parse(`<template></template>`)
  144. expect(descriptor.template).toBeTruthy()
  145. expect(descriptor.template!.content).toBeFalsy()
  146. expect(descriptor.template!.loc).toMatchObject({
  147. start: { line: 1, column: 11, offset: 10 },
  148. end: { line: 1, column: 11, offset: 10 },
  149. source: ''
  150. })
  151. })
  152. test('should ignore other nodes with no content', () => {
  153. expect(parse(`<script/>`).descriptor.script).toBe(null)
  154. expect(parse(`<script> \n\t </script>`).descriptor.script).toBe(null)
  155. expect(parse(`<style/>`).descriptor.styles.length).toBe(0)
  156. expect(parse(`<style> \n\t </style>`).descriptor.styles.length).toBe(0)
  157. expect(parse(`<custom/>`).descriptor.customBlocks.length).toBe(0)
  158. expect(
  159. parse(`<custom> \n\t </custom>`).descriptor.customBlocks.length
  160. ).toBe(0)
  161. })
  162. test('handle empty nodes with src attribute', () => {
  163. const { descriptor } = parse(`<script src="com"/>`)
  164. expect(descriptor.script).toBeTruthy()
  165. expect(descriptor.script!.content).toBeFalsy()
  166. expect(descriptor.script!.attrs['src']).toBe('com')
  167. })
  168. test('ignoreEmpty: false', () => {
  169. const { descriptor } = parse(
  170. `<script></script>\n<script setup>\n</script>`,
  171. {
  172. ignoreEmpty: false
  173. }
  174. )
  175. expect(descriptor.script).toBeTruthy()
  176. expect(descriptor.script!.loc).toMatchObject({
  177. source: '',
  178. start: { line: 1, column: 9, offset: 8 },
  179. end: { line: 1, column: 9, offset: 8 }
  180. })
  181. expect(descriptor.scriptSetup).toBeTruthy()
  182. expect(descriptor.scriptSetup!.loc).toMatchObject({
  183. source: '\n',
  184. start: { line: 2, column: 15, offset: 32 },
  185. end: { line: 3, column: 1, offset: 33 }
  186. })
  187. })
  188. test('nested templates', () => {
  189. const content = `
  190. <template v-if="ok">ok</template>
  191. <div><div></div></div>
  192. `
  193. const { descriptor } = parse(`<template>${content}</template>`)
  194. expect(descriptor.template!.content).toBe(content)
  195. })
  196. test('treat empty lang attribute as the html', () => {
  197. const content = `<div><template v-if="ok">ok</template></div>`
  198. const { descriptor, errors } = parse(
  199. `<template lang="">${content}</template>`
  200. )
  201. expect(descriptor.template!.content).toBe(content)
  202. expect(errors.length).toBe(0)
  203. })
  204. // #1120
  205. test('alternative template lang should be treated as plain text', () => {
  206. const content = `p(v-if="1 < 2") test`
  207. const { descriptor, errors } = parse(
  208. `<template lang="pug">` + content + `</template>`
  209. )
  210. expect(errors.length).toBe(0)
  211. expect(descriptor.template!.content).toBe(content)
  212. })
  213. //#2566
  214. test('div lang should not be treated as plain text', () => {
  215. const { errors } = parse(`
  216. <template lang="pug">
  217. <div lang="">
  218. <div></div>
  219. </div>
  220. </template>
  221. `)
  222. expect(errors.length).toBe(0)
  223. })
  224. test('slotted detection', async () => {
  225. expect(parse(`<template>hi</template>`).descriptor.slotted).toBe(false)
  226. expect(
  227. parse(`<template>hi</template><style>h1{color:red;}</style>`).descriptor
  228. .slotted
  229. ).toBe(false)
  230. expect(
  231. parse(
  232. `<template>hi</template><style scoped>:slotted(h1){color:red;}</style>`
  233. ).descriptor.slotted
  234. ).toBe(true)
  235. expect(
  236. parse(
  237. `<template>hi</template><style scoped>::v-slotted(h1){color:red;}</style>`
  238. ).descriptor.slotted
  239. ).toBe(true)
  240. })
  241. test('error tolerance', () => {
  242. const { errors } = parse(`<template>`)
  243. expect(errors.length).toBe(1)
  244. })
  245. test('should parse as DOM by default', () => {
  246. const { errors } = parse(`<template><input></template>`)
  247. expect(errors.length).toBe(0)
  248. })
  249. test('custom compiler', () => {
  250. const { errors } = parse(`<template><input></template>`, {
  251. compiler: {
  252. parse: baseParse,
  253. compile: baseCompile
  254. }
  255. })
  256. expect(errors.length).toBe(1)
  257. })
  258. test('treat custom blocks as raw text', () => {
  259. const { errors, descriptor } = parse(
  260. `<template><input></template><foo> <-& </foo>`
  261. )
  262. expect(errors.length).toBe(0)
  263. expect(descriptor.customBlocks[0].content).toBe(` <-& `)
  264. })
  265. describe('warnings', () => {
  266. function assertWarning(errors: Error[], msg: string) {
  267. expect(errors.some(e => e.message.match(msg))).toBe(true)
  268. }
  269. test('should only allow single template element', () => {
  270. assertWarning(
  271. parse(`<template><div/></template><template><div/></template>`).errors,
  272. `Single file component can contain only one <template> element`
  273. )
  274. })
  275. test('should only allow single script element', () => {
  276. assertWarning(
  277. parse(`<script>console.log(1)</script><script>console.log(1)</script>`)
  278. .errors,
  279. `Single file component can contain only one <script> element`
  280. )
  281. })
  282. test('should only allow single script setup element', () => {
  283. assertWarning(
  284. parse(
  285. `<script setup>console.log(1)</script><script setup>console.log(1)</script>`
  286. ).errors,
  287. `Single file component can contain only one <script setup> element`
  288. )
  289. })
  290. test('should not warn script & script setup', () => {
  291. expect(
  292. parse(
  293. `<script setup>console.log(1)</script><script>console.log(1)</script>`
  294. ).errors.length
  295. ).toBe(0)
  296. })
  297. // # 6676
  298. test('should throw error if no <template> or <script> is present', () => {
  299. assertWarning(
  300. parse(`import { ref } from 'vue'`).errors,
  301. `At least one <template> or <script> is required in a single file component`
  302. )
  303. })
  304. })
  305. })