compileTemplate.ts 8.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303
  1. import {
  2. CompilerOptions,
  3. CodegenResult,
  4. CompilerError,
  5. NodeTransform,
  6. ParserOptions,
  7. RootNode
  8. } from '@vue/compiler-core'
  9. import { SourceMapConsumer, SourceMapGenerator, RawSourceMap } from 'source-map'
  10. import {
  11. transformAssetUrl,
  12. AssetURLOptions,
  13. createAssetUrlTransformWithOptions,
  14. AssetURLTagConfig,
  15. normalizeOptions
  16. } from './templateTransformAssetUrl'
  17. import {
  18. transformSrcset,
  19. createSrcsetTransformWithOptions
  20. } from './templateTransformSrcset'
  21. import { isObject } from '@vue/shared'
  22. import * as CompilerDOM from '@vue/compiler-dom'
  23. import * as CompilerSSR from '@vue/compiler-ssr'
  24. import consolidate from 'consolidate'
  25. import { warnOnce } from './warn'
  26. import { genCssVarsFromList } from './cssVars'
  27. export interface TemplateCompiler {
  28. compile(template: string, options: CompilerOptions): CodegenResult
  29. parse(template: string, options: ParserOptions): RootNode
  30. }
  31. export interface SFCTemplateCompileResults {
  32. code: string
  33. ast?: RootNode
  34. preamble?: string
  35. source: string
  36. tips: string[]
  37. errors: (string | CompilerError)[]
  38. map?: RawSourceMap
  39. }
  40. export interface SFCTemplateCompileOptions {
  41. source: string
  42. filename: string
  43. id: string
  44. scoped?: boolean
  45. slotted?: boolean
  46. isProd?: boolean
  47. ssr?: boolean
  48. ssrCssVars?: string[]
  49. inMap?: RawSourceMap
  50. compiler?: TemplateCompiler
  51. compilerOptions?: CompilerOptions
  52. preprocessLang?: string
  53. preprocessOptions?: any
  54. /**
  55. * In some cases, compiler-sfc may not be inside the project root (e.g. when
  56. * linked or globally installed). In such cases a custom `require` can be
  57. * passed to correctly resolve the preprocessors.
  58. */
  59. preprocessCustomRequire?: (id: string) => any
  60. /**
  61. * Configure what tags/attributes to transform into asset url imports,
  62. * or disable the transform altogether with `false`.
  63. */
  64. transformAssetUrls?: AssetURLOptions | AssetURLTagConfig | boolean
  65. }
  66. interface PreProcessor {
  67. render(
  68. source: string,
  69. options: any,
  70. cb: (err: Error | null, res: string) => void
  71. ): void
  72. }
  73. function preprocess(
  74. { source, filename, preprocessOptions }: SFCTemplateCompileOptions,
  75. preprocessor: PreProcessor
  76. ): string {
  77. // Consolidate exposes a callback based API, but the callback is in fact
  78. // called synchronously for most templating engines. In our case, we have to
  79. // expose a synchronous API so that it is usable in Jest transforms (which
  80. // have to be sync because they are applied via Node.js require hooks)
  81. let res: string = ''
  82. let err: Error | null = null
  83. preprocessor.render(
  84. source,
  85. { filename, ...preprocessOptions },
  86. (_err, _res) => {
  87. if (_err) err = _err
  88. res = _res
  89. }
  90. )
  91. if (err) throw err
  92. return res
  93. }
  94. export function compileTemplate(
  95. options: SFCTemplateCompileOptions
  96. ): SFCTemplateCompileResults {
  97. const { preprocessLang, preprocessCustomRequire } = options
  98. if (
  99. (__ESM_BROWSER__ || __GLOBAL__) &&
  100. preprocessLang &&
  101. !preprocessCustomRequire
  102. ) {
  103. throw new Error(
  104. `[@vue/compiler-sfc] Template preprocessing in the browser build must ` +
  105. `provide the \`preprocessCustomRequire\` option to return the in-browser ` +
  106. `version of the preprocessor in the shape of { render(): string }.`
  107. )
  108. }
  109. const preprocessor = preprocessLang
  110. ? preprocessCustomRequire
  111. ? preprocessCustomRequire(preprocessLang)
  112. : require('consolidate')[preprocessLang as keyof typeof consolidate]
  113. : false
  114. if (preprocessor) {
  115. try {
  116. return doCompileTemplate({
  117. ...options,
  118. source: preprocess(options, preprocessor)
  119. })
  120. } catch (e) {
  121. return {
  122. code: `export default function render() {}`,
  123. source: options.source,
  124. tips: [],
  125. errors: [e]
  126. }
  127. }
  128. } else if (preprocessLang) {
  129. return {
  130. code: `export default function render() {}`,
  131. source: options.source,
  132. tips: [
  133. `Component ${
  134. options.filename
  135. } uses lang ${preprocessLang} for template. Please install the language preprocessor.`
  136. ],
  137. errors: [
  138. `Component ${
  139. options.filename
  140. } uses lang ${preprocessLang} for template, however it is not installed.`
  141. ]
  142. }
  143. } else {
  144. return doCompileTemplate(options)
  145. }
  146. }
  147. function doCompileTemplate({
  148. filename,
  149. id,
  150. scoped,
  151. slotted,
  152. inMap,
  153. source,
  154. ssr = false,
  155. ssrCssVars,
  156. isProd = false,
  157. compiler = ssr ? (CompilerSSR as TemplateCompiler) : CompilerDOM,
  158. compilerOptions = {},
  159. transformAssetUrls
  160. }: SFCTemplateCompileOptions): SFCTemplateCompileResults {
  161. const errors: CompilerError[] = []
  162. let nodeTransforms: NodeTransform[] = []
  163. if (isObject(transformAssetUrls)) {
  164. const assetOptions = normalizeOptions(transformAssetUrls)
  165. nodeTransforms = [
  166. createAssetUrlTransformWithOptions(assetOptions),
  167. createSrcsetTransformWithOptions(assetOptions)
  168. ]
  169. } else if (transformAssetUrls !== false) {
  170. nodeTransforms = [transformAssetUrl, transformSrcset]
  171. }
  172. if (ssr && !ssrCssVars) {
  173. warnOnce(
  174. `compileTemplate is called with \`ssr: true\` but no ` +
  175. `corresponding \`cssVars\` option.\`.`
  176. )
  177. }
  178. if (!id) {
  179. warnOnce(`compileTemplate now requires the \`id\` option.\`.`)
  180. id = ''
  181. }
  182. const shortId = id.replace(/^data-v-/, '')
  183. const longId = `data-v-${shortId}`
  184. let { code, ast, preamble, map } = compiler.compile(source, {
  185. mode: 'module',
  186. prefixIdentifiers: true,
  187. hoistStatic: true,
  188. cacheHandlers: true,
  189. ssrCssVars:
  190. ssr && ssrCssVars && ssrCssVars.length
  191. ? genCssVarsFromList(ssrCssVars, shortId, isProd)
  192. : '',
  193. scopeId: scoped ? longId : undefined,
  194. slotted,
  195. ...compilerOptions,
  196. nodeTransforms: nodeTransforms.concat(compilerOptions.nodeTransforms || []),
  197. filename,
  198. sourceMap: true,
  199. onError: e => errors.push(e)
  200. })
  201. // inMap should be the map produced by ./parse.ts which is a simple line-only
  202. // mapping. If it is present, we need to adjust the final map and errors to
  203. // reflect the original line numbers.
  204. if (inMap) {
  205. if (map) {
  206. map = mapLines(inMap, map)
  207. }
  208. if (errors.length) {
  209. patchErrors(errors, source, inMap)
  210. }
  211. }
  212. return { code, ast, preamble, source, errors, tips: [], map }
  213. }
  214. function mapLines(oldMap: RawSourceMap, newMap: RawSourceMap): RawSourceMap {
  215. if (!oldMap) return newMap
  216. if (!newMap) return oldMap
  217. const oldMapConsumer = new SourceMapConsumer(oldMap)
  218. const newMapConsumer = new SourceMapConsumer(newMap)
  219. const mergedMapGenerator = new SourceMapGenerator()
  220. newMapConsumer.eachMapping(m => {
  221. if (m.originalLine == null) {
  222. return
  223. }
  224. const origPosInOldMap = oldMapConsumer.originalPositionFor({
  225. line: m.originalLine,
  226. column: m.originalColumn
  227. })
  228. if (origPosInOldMap.source == null) {
  229. return
  230. }
  231. mergedMapGenerator.addMapping({
  232. generated: {
  233. line: m.generatedLine,
  234. column: m.generatedColumn
  235. },
  236. original: {
  237. line: origPosInOldMap.line, // map line
  238. // use current column, since the oldMap produced by @vue/compiler-sfc
  239. // does not
  240. column: m.originalColumn
  241. },
  242. source: origPosInOldMap.source,
  243. name: origPosInOldMap.name
  244. })
  245. })
  246. // source-map's type definition is incomplete
  247. const generator = mergedMapGenerator as any
  248. ;(oldMapConsumer as any).sources.forEach((sourceFile: string) => {
  249. generator._sources.add(sourceFile)
  250. const sourceContent = oldMapConsumer.sourceContentFor(sourceFile)
  251. if (sourceContent != null) {
  252. mergedMapGenerator.setSourceContent(sourceFile, sourceContent)
  253. }
  254. })
  255. generator._sourceRoot = oldMap.sourceRoot
  256. generator._file = oldMap.file
  257. return generator.toJSON()
  258. }
  259. function patchErrors(
  260. errors: CompilerError[],
  261. source: string,
  262. inMap: RawSourceMap
  263. ) {
  264. const originalSource = inMap.sourcesContent![0]
  265. const offset = originalSource.indexOf(source)
  266. const lineOffset = originalSource.slice(0, offset).split(/\r?\n/).length - 1
  267. errors.forEach(err => {
  268. if (err.loc) {
  269. err.loc.start.line += lineOffset
  270. err.loc.start.offset += offset
  271. if (err.loc.end !== err.loc.start) {
  272. err.loc.end.line += lineOffset
  273. err.loc.end.offset += offset
  274. }
  275. }
  276. })
  277. }