parse.spec.ts 12 KB

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