compileTemplate.ts 7.2 KB

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