compileTemplate.ts 8.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315
  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 { generateCodeFrame, isObject } from '@vue/shared'
  22. import * as CompilerDOM from '@vue/compiler-dom'
  23. import * as CompilerSSR from '@vue/compiler-ssr'
  24. import consolidate from '@vue/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. : __ESM_BROWSER__
  113. ? undefined
  114. : consolidate[preprocessLang as keyof typeof consolidate]
  115. : false
  116. if (preprocessor) {
  117. try {
  118. return doCompileTemplate({
  119. ...options,
  120. source: preprocess(options, preprocessor)
  121. })
  122. } catch (e: any) {
  123. return {
  124. code: `export default function render() {}`,
  125. source: options.source,
  126. tips: [],
  127. errors: [e]
  128. }
  129. }
  130. } else if (preprocessLang) {
  131. return {
  132. code: `export default function render() {}`,
  133. source: options.source,
  134. tips: [
  135. `Component ${options.filename} uses lang ${preprocessLang} for template. Please install the language preprocessor.`
  136. ],
  137. errors: [
  138. `Component ${options.filename} uses lang ${preprocessLang} for template, however it is not installed.`
  139. ]
  140. }
  141. } else {
  142. return doCompileTemplate(options)
  143. }
  144. }
  145. function doCompileTemplate({
  146. filename,
  147. id,
  148. scoped,
  149. slotted,
  150. inMap,
  151. source,
  152. ssr = false,
  153. ssrCssVars,
  154. isProd = false,
  155. compiler = ssr ? (CompilerSSR as TemplateCompiler) : CompilerDOM,
  156. compilerOptions = {},
  157. transformAssetUrls
  158. }: SFCTemplateCompileOptions): SFCTemplateCompileResults {
  159. const errors: CompilerError[] = []
  160. const warnings: CompilerError[] = []
  161. let nodeTransforms: NodeTransform[] = []
  162. if (isObject(transformAssetUrls)) {
  163. const assetOptions = normalizeOptions(transformAssetUrls)
  164. nodeTransforms = [
  165. createAssetUrlTransformWithOptions(assetOptions),
  166. createSrcsetTransformWithOptions(assetOptions)
  167. ]
  168. } else if (transformAssetUrls !== false) {
  169. nodeTransforms = [transformAssetUrl, transformSrcset]
  170. }
  171. if (ssr && !ssrCssVars) {
  172. warnOnce(
  173. `compileTemplate is called with \`ssr: true\` but no ` +
  174. `corresponding \`cssVars\` option.\`.`
  175. )
  176. }
  177. if (!id) {
  178. warnOnce(`compileTemplate now requires the \`id\` option.\`.`)
  179. id = ''
  180. }
  181. const shortId = id.replace(/^data-v-/, '')
  182. const longId = `data-v-${shortId}`
  183. let { code, ast, preamble, map } = compiler.compile(source, {
  184. mode: 'module',
  185. prefixIdentifiers: true,
  186. hoistStatic: true,
  187. cacheHandlers: true,
  188. ssrCssVars:
  189. ssr && ssrCssVars && ssrCssVars.length
  190. ? genCssVarsFromList(ssrCssVars, shortId, isProd)
  191. : '',
  192. scopeId: scoped ? longId : undefined,
  193. slotted,
  194. sourceMap: true,
  195. ...compilerOptions,
  196. nodeTransforms: nodeTransforms.concat(compilerOptions.nodeTransforms || []),
  197. filename,
  198. onError: e => errors.push(e),
  199. onWarn: w => warnings.push(w)
  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. const tips = warnings.map(w => {
  213. let msg = w.message
  214. if (w.loc) {
  215. msg += `\n${generateCodeFrame(
  216. source,
  217. w.loc.start.offset,
  218. w.loc.end.offset
  219. )}`
  220. }
  221. return msg
  222. })
  223. return { code, ast, preamble, source, errors, tips, map }
  224. }
  225. function mapLines(oldMap: RawSourceMap, newMap: RawSourceMap): RawSourceMap {
  226. if (!oldMap) return newMap
  227. if (!newMap) return oldMap
  228. const oldMapConsumer = new SourceMapConsumer(oldMap)
  229. const newMapConsumer = new SourceMapConsumer(newMap)
  230. const mergedMapGenerator = new SourceMapGenerator()
  231. newMapConsumer.eachMapping(m => {
  232. if (m.originalLine == null) {
  233. return
  234. }
  235. const origPosInOldMap = oldMapConsumer.originalPositionFor({
  236. line: m.originalLine,
  237. column: m.originalColumn
  238. })
  239. if (origPosInOldMap.source == null) {
  240. return
  241. }
  242. mergedMapGenerator.addMapping({
  243. generated: {
  244. line: m.generatedLine,
  245. column: m.generatedColumn
  246. },
  247. original: {
  248. line: origPosInOldMap.line, // map line
  249. // use current column, since the oldMap produced by @vue/compiler-sfc
  250. // does not
  251. column: m.originalColumn
  252. },
  253. source: origPosInOldMap.source,
  254. name: origPosInOldMap.name
  255. })
  256. })
  257. // source-map's type definition is incomplete
  258. const generator = mergedMapGenerator as any
  259. ;(oldMapConsumer as any).sources.forEach((sourceFile: string) => {
  260. generator._sources.add(sourceFile)
  261. const sourceContent = oldMapConsumer.sourceContentFor(sourceFile)
  262. if (sourceContent != null) {
  263. mergedMapGenerator.setSourceContent(sourceFile, sourceContent)
  264. }
  265. })
  266. generator._sourceRoot = oldMap.sourceRoot
  267. generator._file = oldMap.file
  268. return generator.toJSON()
  269. }
  270. function patchErrors(
  271. errors: CompilerError[],
  272. source: string,
  273. inMap: RawSourceMap
  274. ) {
  275. const originalSource = inMap.sourcesContent![0]
  276. const offset = originalSource.indexOf(source)
  277. const lineOffset = originalSource.slice(0, offset).split(/\r?\n/).length - 1
  278. errors.forEach(err => {
  279. if (err.loc) {
  280. err.loc.start.line += lineOffset
  281. err.loc.start.offset += offset
  282. if (err.loc.end !== err.loc.start) {
  283. err.loc.end.line += lineOffset
  284. err.loc.end.offset += offset
  285. }
  286. }
  287. })
  288. }