compileTemplate.spec.ts 13 KB

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