compileTemplate.ts 6.0 KB

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