compileTemplate.ts 7.3 KB

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