compileTemplate.spec.ts 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486
  1. import { type RawSourceMap, SourceMapConsumer } from 'source-map-js'
  2. import {
  3. type SFCTemplateCompileOptions,
  4. compileTemplate,
  5. } from '../src/compileTemplate'
  6. import { type SFCTemplateBlock, parse } from '../src/parse'
  7. import { compileScript } from '../src'
  8. function compile(opts: Omit<SFCTemplateCompileOptions, 'id'>) {
  9. return compileTemplate({
  10. ...opts,
  11. id: '',
  12. })
  13. }
  14. test('should work', () => {
  15. const source = `<div><p>{{ render }}</p></div>`
  16. const result = compile({ filename: 'example.vue', source })
  17. expect(result.errors.length).toBe(0)
  18. expect(result.source).toBe(source)
  19. // should expose render fn
  20. expect(result.code).toMatch(`export function render(`)
  21. })
  22. // #6807
  23. test('should work with style comment', () => {
  24. const source = `
  25. <div style="
  26. /* nothing */
  27. width: 300px;
  28. height: 100px/* nothing */
  29. ">{{ render }}</div>
  30. `
  31. const result = compile({ filename: 'example.vue', source })
  32. expect(result.errors.length).toBe(0)
  33. expect(result.source).toBe(source)
  34. expect(result.code).toMatch(`{"width":"300px","height":"100px"}`)
  35. })
  36. test('preprocess pug', () => {
  37. const template = parse(
  38. `
  39. <template lang="pug">
  40. body
  41. h1 Pug Examples
  42. div.container
  43. p Cool Pug example!
  44. </template>
  45. `,
  46. { filename: 'example.vue', sourceMap: true },
  47. ).descriptor.template as SFCTemplateBlock
  48. const result = compile({
  49. filename: 'example.vue',
  50. source: template.content,
  51. preprocessLang: template.lang,
  52. })
  53. expect(result.errors.length).toBe(0)
  54. })
  55. test('preprocess pug with indents and blank lines', () => {
  56. const template = parse(
  57. `
  58. <template lang="pug">
  59. body
  60. h1 The next line contains four spaces.
  61. div.container
  62. p The next line is empty.
  63. p This is the last line.
  64. </template>
  65. `,
  66. { filename: 'example.vue', sourceMap: true },
  67. ).descriptor.template as SFCTemplateBlock
  68. const result = compile({
  69. filename: 'example.vue',
  70. source: template.content,
  71. preprocessLang: template.lang,
  72. })
  73. expect(result.errors.length).toBe(0)
  74. expect(result.source).toBe(
  75. '<body><h1>The next line contains four spaces.</h1><div class="container"><p>The next line is empty.</p></div><p>This is the last line.</p></body>',
  76. )
  77. })
  78. test('warn missing preprocessor', () => {
  79. const template = parse(`<template lang="unknownLang">hi</template>\n`, {
  80. filename: 'example.vue',
  81. sourceMap: true,
  82. }).descriptor.template as SFCTemplateBlock
  83. const result = compile({
  84. filename: 'example.vue',
  85. source: template.content,
  86. preprocessLang: template.lang,
  87. })
  88. expect(result.errors.length).toBe(1)
  89. })
  90. test('transform asset url options', () => {
  91. const input = { source: `<foo bar="~baz"/>`, filename: 'example.vue' }
  92. // Object option
  93. const { code: code1 } = compile({
  94. ...input,
  95. transformAssetUrls: {
  96. tags: { foo: ['bar'] },
  97. },
  98. })
  99. expect(code1).toMatch(`import _imports_0 from 'baz'\n`)
  100. // legacy object option (direct tags config)
  101. const { code: code2 } = compile({
  102. ...input,
  103. transformAssetUrls: {
  104. foo: ['bar'],
  105. },
  106. })
  107. expect(code2).toMatch(`import _imports_0 from 'baz'\n`)
  108. // false option
  109. const { code: code3 } = compile({
  110. ...input,
  111. transformAssetUrls: false,
  112. })
  113. expect(code3).not.toMatch(`import _imports_0 from 'baz'\n`)
  114. })
  115. test('source map', () => {
  116. const template = parse(
  117. `
  118. <template>
  119. <div><p>{{ foobar }}</p></div>
  120. </template>
  121. `,
  122. { filename: 'example.vue', sourceMap: true },
  123. ).descriptor.template!
  124. const { code, map } = compile({
  125. filename: 'example.vue',
  126. source: template.content,
  127. })
  128. expect(map!.sources).toEqual([`example.vue`])
  129. expect(map!.sourcesContent).toEqual([template.content])
  130. const consumer = new SourceMapConsumer(map as RawSourceMap)
  131. expect(
  132. consumer.originalPositionFor(getPositionInCode(code, 'foobar')),
  133. ).toMatchObject(getPositionInCode(template.content, `foobar`))
  134. })
  135. test('should work w/ AST from descriptor', () => {
  136. const source = `
  137. <template>
  138. <div><p>{{ foobar }}</p></div>
  139. </template>
  140. `
  141. const template = parse(source, {
  142. filename: 'example.vue',
  143. sourceMap: true,
  144. }).descriptor.template!
  145. expect(template.ast!.source).toBe(source)
  146. const { code, map } = compile({
  147. filename: 'example.vue',
  148. source: template.content,
  149. ast: template.ast,
  150. })
  151. expect(map!.sources).toEqual([`example.vue`])
  152. // when reusing AST from SFC parse for template compile,
  153. // the source corresponds to the entire SFC
  154. expect(map!.sourcesContent).toEqual([source])
  155. const consumer = new SourceMapConsumer(map as RawSourceMap)
  156. expect(
  157. consumer.originalPositionFor(getPositionInCode(code, 'foobar')),
  158. ).toMatchObject(getPositionInCode(source, `foobar`))
  159. expect(code).toBe(
  160. compile({
  161. filename: 'example.vue',
  162. source: template.content,
  163. }).code,
  164. )
  165. })
  166. test('should work w/ AST from descriptor in SSR mode', () => {
  167. const source = `
  168. <template>
  169. <div><p>{{ foobar }}</p></div>
  170. </template>
  171. `
  172. const template = parse(source, {
  173. filename: 'example.vue',
  174. sourceMap: true,
  175. }).descriptor.template!
  176. expect(template.ast!.source).toBe(source)
  177. const { code, map } = compile({
  178. filename: 'example.vue',
  179. source: '', // make sure it's actually using the AST instead of source
  180. ast: template.ast,
  181. ssr: true,
  182. })
  183. expect(map!.sources).toEqual([`example.vue`])
  184. // when reusing AST from SFC parse for template compile,
  185. // the source corresponds to the entire SFC
  186. expect(map!.sourcesContent).toEqual([source])
  187. const consumer = new SourceMapConsumer(map as RawSourceMap)
  188. expect(
  189. consumer.originalPositionFor(getPositionInCode(code, 'foobar')),
  190. ).toMatchObject(getPositionInCode(source, `foobar`))
  191. expect(code).toBe(
  192. compile({
  193. filename: 'example.vue',
  194. source: template.content,
  195. ssr: true,
  196. }).code,
  197. )
  198. })
  199. test('should not reuse AST if using custom compiler', () => {
  200. const source = `
  201. <template>
  202. <div><p>{{ foobar }}</p></div>
  203. </template>
  204. `
  205. const template = parse(source, {
  206. filename: 'example.vue',
  207. sourceMap: true,
  208. }).descriptor.template!
  209. const { code } = compile({
  210. filename: 'example.vue',
  211. source: template.content,
  212. ast: template.ast,
  213. compiler: {
  214. parse: () => null as any,
  215. // @ts-expect-error
  216. compile: input => ({ code: input }),
  217. },
  218. })
  219. // what we really want to assert is that the `input` received by the custom
  220. // compiler is the source string, not the AST.
  221. expect(code).toBe(template.content)
  222. })
  223. test('should force re-parse on already transformed AST', () => {
  224. const source = `
  225. <template>
  226. <div><p>{{ foobar }}</p></div>
  227. </template>
  228. `
  229. const template = parse(source, {
  230. filename: 'example.vue',
  231. sourceMap: true,
  232. }).descriptor.template!
  233. // force set to empty, if this is reused then it won't generate proper code
  234. template.ast!.children = []
  235. template.ast!.transformed = true
  236. const { code } = compile({
  237. filename: 'example.vue',
  238. source: '',
  239. ast: template.ast,
  240. })
  241. expect(code).toBe(
  242. compile({
  243. filename: 'example.vue',
  244. source: template.content,
  245. }).code,
  246. )
  247. })
  248. test('should force re-parse with correct compiler in SSR mode', () => {
  249. const source = `
  250. <template>
  251. <div><p>{{ foobar }}</p></div>
  252. </template>
  253. `
  254. const template = parse(source, {
  255. filename: 'example.vue',
  256. sourceMap: true,
  257. }).descriptor.template!
  258. // force set to empty, if this is reused then it won't generate proper code
  259. template.ast!.children = []
  260. template.ast!.transformed = true
  261. const { code } = compile({
  262. filename: 'example.vue',
  263. source: '',
  264. ast: template.ast,
  265. ssr: true,
  266. })
  267. expect(code).toBe(
  268. compile({
  269. filename: 'example.vue',
  270. source: template.content,
  271. ssr: true,
  272. }).code,
  273. )
  274. })
  275. test('template errors', () => {
  276. const result = compile({
  277. filename: 'example.vue',
  278. source: `<div
  279. :bar="a[" v-model="baz"/>`,
  280. })
  281. expect(result.errors).toMatchSnapshot()
  282. })
  283. test('preprocessor errors', () => {
  284. const template = parse(
  285. `
  286. <template lang="pug">
  287. div(class='class)
  288. </template>
  289. `,
  290. { filename: 'example.vue', sourceMap: true },
  291. ).descriptor.template as SFCTemplateBlock
  292. const result = compile({
  293. filename: 'example.vue',
  294. source: template.content,
  295. preprocessLang: template.lang,
  296. })
  297. expect(result.errors.length).toBe(1)
  298. const message = result.errors[0].toString()
  299. expect(message).toMatch(`Error: example.vue:3:1`)
  300. expect(message).toMatch(
  301. `The end of the string reached with no closing bracket ) found.`,
  302. )
  303. })
  304. // #3447
  305. test('should generate the correct imports expression', () => {
  306. const { code } = compile({
  307. filename: 'example.vue',
  308. source: `
  309. <img src="./foo.svg"/>
  310. <Comp>
  311. <img src="./bar.svg"/>
  312. </Comp>
  313. `,
  314. ssr: true,
  315. })
  316. expect(code).toMatch(`_ssrRenderAttr(\"src\", _imports_1)`)
  317. expect(code).toMatch(`_createVNode(\"img\", { src: _imports_1 })`)
  318. })
  319. // #3874
  320. test('should not hoist srcset URLs in SSR mode', () => {
  321. const { code } = compile({
  322. filename: 'example.vue',
  323. source: `
  324. <picture>
  325. <source srcset="./img/foo.svg"/>
  326. <img src="./img/foo.svg"/>
  327. </picture>
  328. <router-link>
  329. <picture>
  330. <source srcset="./img/bar.svg"/>
  331. <img src="./img/bar.svg"/>
  332. </picture>
  333. </router-link>
  334. `,
  335. ssr: true,
  336. })
  337. expect(code).toMatchSnapshot()
  338. })
  339. // #6742
  340. test('dynamic v-on + static v-on should merged', () => {
  341. const source = `<input @blur="onBlur" @[validateEvent]="onValidateEvent">`
  342. const result = compile({ filename: 'example.vue', source })
  343. expect(result.code).toMatchSnapshot()
  344. })
  345. // #9853 regression found in Nuxt tests
  346. // walkIdentifiers can get called multiple times on the same node
  347. // due to #9729 calling it during SFC template usage check.
  348. // conditions needed:
  349. // 1. `<script setup lang="ts">`
  350. // 2. Has import
  351. // 3. inlineTemplate: false
  352. // 4. AST being reused
  353. test('prefixing edge case for reused AST', () => {
  354. const src = `
  355. <script setup lang="ts">
  356. import { Foo } from './foo'
  357. </script>
  358. <template>
  359. {{ list.map((t, index) => ({ t: t })) }}
  360. </template>
  361. `
  362. const { descriptor } = parse(src)
  363. // compileScript triggers importUsageCheck
  364. compileScript(descriptor, { id: 'xxx' })
  365. const { code } = compileTemplate({
  366. id: 'xxx',
  367. filename: 'test.vue',
  368. ast: descriptor.template!.ast,
  369. source: descriptor.template!.content,
  370. })
  371. expect(code).not.toMatch(`_ctx.t`)
  372. })
  373. test('prefixing edge case for reused AST ssr mode', () => {
  374. const src = `
  375. <script setup lang="ts">
  376. import { Foo } from './foo'
  377. </script>
  378. <template>
  379. <Bar>
  380. <template #option="{ foo }"></template>
  381. </Bar>
  382. </template>
  383. `
  384. const { descriptor } = parse(src)
  385. // compileScript triggers importUsageCheck
  386. compileScript(descriptor, { id: 'xxx' })
  387. expect(() =>
  388. compileTemplate({
  389. id: 'xxx',
  390. filename: 'test.vue',
  391. ast: descriptor.template!.ast,
  392. source: descriptor.template!.content,
  393. ssr: true,
  394. }),
  395. ).not.toThrowError()
  396. })
  397. interface Pos {
  398. line: number
  399. column: number
  400. name?: string
  401. }
  402. function getPositionInCode(
  403. code: string,
  404. token: string,
  405. expectName: string | boolean = false,
  406. ): Pos {
  407. const generatedOffset = code.indexOf(token)
  408. let line = 1
  409. let lastNewLinePos = -1
  410. for (let i = 0; i < generatedOffset; i++) {
  411. if (code.charCodeAt(i) === 10 /* newline char code */) {
  412. line++
  413. lastNewLinePos = i
  414. }
  415. }
  416. const res: Pos = {
  417. line,
  418. column:
  419. lastNewLinePos === -1
  420. ? generatedOffset
  421. : generatedOffset - lastNewLinePos - 1,
  422. }
  423. if (expectName) {
  424. res.name = typeof expectName === 'string' ? expectName : token
  425. }
  426. return res
  427. }