compileTemplate.ts 5.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223
  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. } from './templateTransformAssetUrl'
  15. import { transformSrcset } from './templateTransformSrcset'
  16. import { isObject } from '@vue/shared'
  17. import consolidate from 'consolidate'
  18. export interface TemplateCompiler {
  19. compile(template: string, options: CompilerOptions): CodegenResult
  20. parse(template: string, options: ParserOptions): RootNode
  21. }
  22. export interface SFCTemplateCompileResults {
  23. code: string
  24. source: string
  25. tips: string[]
  26. errors: (string | CompilerError)[]
  27. map?: RawSourceMap
  28. }
  29. export interface SFCTemplateCompileOptions {
  30. source: string
  31. filename: string
  32. inMap?: RawSourceMap
  33. compiler?: TemplateCompiler
  34. compilerOptions?: CompilerOptions
  35. preprocessLang?: string
  36. preprocessOptions?: any
  37. transformAssetUrls?: AssetURLOptions | boolean
  38. }
  39. function preprocess(
  40. { source, filename, preprocessOptions }: SFCTemplateCompileOptions,
  41. preprocessor: any
  42. ): string {
  43. // Consolidate exposes a callback based API, but the callback is in fact
  44. // called synchronously for most templating engines. In our case, we have to
  45. // expose a synchronous API so that it is usable in Jest transforms (which
  46. // have to be sync because they are applied via Node.js require hooks)
  47. let res: any, err
  48. preprocessor.render(
  49. source,
  50. { filename, ...preprocessOptions },
  51. (_err: Error | null, _res: string) => {
  52. if (_err) err = _err
  53. res = _res
  54. }
  55. )
  56. if (err) throw err
  57. return res
  58. }
  59. export function compileTemplate(
  60. options: SFCTemplateCompileOptions
  61. ): SFCTemplateCompileResults {
  62. const { preprocessLang } = options
  63. const preprocessor =
  64. preprocessLang && consolidate[preprocessLang as keyof typeof consolidate]
  65. if (preprocessor) {
  66. try {
  67. return doCompileTemplate({
  68. ...options,
  69. source: preprocess(options, preprocessor)
  70. })
  71. } catch (e) {
  72. return {
  73. code: `export default function render() {}`,
  74. source: options.source,
  75. tips: [],
  76. errors: [e]
  77. }
  78. }
  79. } else if (preprocessLang) {
  80. return {
  81. code: `export default function render() {}`,
  82. source: options.source,
  83. tips: [
  84. `Component ${
  85. options.filename
  86. } uses lang ${preprocessLang} for template. Please install the language preprocessor.`
  87. ],
  88. errors: [
  89. `Component ${
  90. options.filename
  91. } uses lang ${preprocessLang} for template, however it is not installed.`
  92. ]
  93. }
  94. } else {
  95. return doCompileTemplate(options)
  96. }
  97. }
  98. function doCompileTemplate({
  99. filename,
  100. inMap,
  101. source,
  102. compiler = require('@vue/compiler-dom'),
  103. compilerOptions = {},
  104. transformAssetUrls
  105. }: SFCTemplateCompileOptions): SFCTemplateCompileResults {
  106. const errors: CompilerError[] = []
  107. let nodeTransforms: NodeTransform[] = []
  108. if (isObject(transformAssetUrls)) {
  109. nodeTransforms = [
  110. createAssetUrlTransformWithOptions(transformAssetUrls),
  111. transformSrcset
  112. ]
  113. } else if (transformAssetUrls !== false) {
  114. nodeTransforms = [transformAssetUrl, transformSrcset]
  115. }
  116. let { code, map } = compiler.compile(source, {
  117. mode: 'module',
  118. prefixIdentifiers: true,
  119. hoistStatic: true,
  120. cacheHandlers: true,
  121. ...compilerOptions,
  122. nodeTransforms: nodeTransforms.concat(compilerOptions.nodeTransforms || []),
  123. filename,
  124. sourceMap: true,
  125. onError: e => errors.push(e)
  126. })
  127. // inMap should be the map produced by ./parse.ts which is a simple line-only
  128. // mapping. If it is present, we need to adjust the final map and errors to
  129. // reflect the original line numbers.
  130. if (inMap) {
  131. if (map) {
  132. map = mapLines(inMap, map)
  133. }
  134. if (errors.length) {
  135. patchErrors(errors, source, inMap)
  136. }
  137. }
  138. return { code, source, errors, tips: [], map }
  139. }
  140. function mapLines(oldMap: RawSourceMap, newMap: RawSourceMap): RawSourceMap {
  141. if (!oldMap) return newMap
  142. if (!newMap) return oldMap
  143. const oldMapConsumer = new SourceMapConsumer(oldMap)
  144. const newMapConsumer = new SourceMapConsumer(newMap)
  145. const mergedMapGenerator = new SourceMapGenerator()
  146. newMapConsumer.eachMapping(m => {
  147. if (m.originalLine == null) {
  148. return
  149. }
  150. const origPosInOldMap = oldMapConsumer.originalPositionFor({
  151. line: m.originalLine,
  152. column: m.originalColumn
  153. })
  154. if (origPosInOldMap.source == null) {
  155. return
  156. }
  157. mergedMapGenerator.addMapping({
  158. generated: {
  159. line: m.generatedLine,
  160. column: m.generatedColumn
  161. },
  162. original: {
  163. line: origPosInOldMap.line, // map line
  164. // use current column, since the oldMap produced by @vue/compiler-sfc
  165. // does not
  166. column: m.originalColumn
  167. },
  168. source: origPosInOldMap.source,
  169. name: origPosInOldMap.name
  170. })
  171. })
  172. // source-map's type definition is incomplete
  173. const generator = mergedMapGenerator as any
  174. ;(oldMapConsumer as any).sources.forEach((sourceFile: string) => {
  175. generator._sources.add(sourceFile)
  176. const sourceContent = oldMapConsumer.sourceContentFor(sourceFile)
  177. if (sourceContent != null) {
  178. mergedMapGenerator.setSourceContent(sourceFile, sourceContent)
  179. }
  180. })
  181. generator._sourceRoot = oldMap.sourceRoot
  182. generator._file = oldMap.file
  183. return generator.toJSON()
  184. }
  185. function patchErrors(
  186. errors: CompilerError[],
  187. source: string,
  188. inMap: RawSourceMap
  189. ) {
  190. const originalSource = inMap.sourcesContent![0]
  191. const offset = originalSource.indexOf(source)
  192. const lineOffset = originalSource.slice(0, offset).split(/\r?\n/).length - 1
  193. errors.forEach(err => {
  194. if (err.loc) {
  195. err.loc.start.line += lineOffset
  196. err.loc.start.offset += offset
  197. if (err.loc.end !== err.loc.start) {
  198. err.loc.end.line += lineOffset
  199. err.loc.end.offset += offset
  200. }
  201. }
  202. })
  203. }