compileTemplate.ts 9.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348
  1. import {
  2. type CodegenResult,
  3. type CompilerError,
  4. type CompilerOptions,
  5. type ElementNode,
  6. type NodeTransform,
  7. NodeTypes,
  8. type ParserOptions,
  9. type RawSourceMap,
  10. type RootNode,
  11. createRoot,
  12. } from '@vue/compiler-core'
  13. import { SourceMapConsumer, SourceMapGenerator } from 'source-map-js'
  14. import {
  15. type AssetURLOptions,
  16. type AssetURLTagConfig,
  17. createAssetUrlTransformWithOptions,
  18. normalizeOptions,
  19. transformAssetUrl,
  20. } from './template/transformAssetUrl'
  21. import {
  22. createSrcsetTransformWithOptions,
  23. transformSrcset,
  24. } from './template/transformSrcset'
  25. import { generateCodeFrame, isObject } from '@vue/shared'
  26. import * as CompilerDOM from '@vue/compiler-dom'
  27. import * as CompilerSSR from '@vue/compiler-ssr'
  28. import consolidate from '@vue/consolidate'
  29. import { warnOnce } from './warn'
  30. import { genCssVarsFromList } from './style/cssVars'
  31. export interface TemplateCompiler {
  32. compile(source: string | RootNode, options: CompilerOptions): CodegenResult
  33. parse(template: string, options: ParserOptions): RootNode
  34. }
  35. export interface SFCTemplateCompileResults {
  36. code: string
  37. ast?: RootNode
  38. preamble?: string
  39. source: string
  40. tips: string[]
  41. errors: (string | CompilerError)[]
  42. map?: RawSourceMap
  43. }
  44. export interface SFCTemplateCompileOptions {
  45. source: string
  46. ast?: RootNode
  47. filename: string
  48. id: string
  49. scoped?: boolean
  50. slotted?: boolean
  51. isProd?: boolean
  52. ssr?: boolean
  53. ssrCssVars?: string[]
  54. inMap?: RawSourceMap
  55. compiler?: TemplateCompiler
  56. compilerOptions?: CompilerOptions
  57. preprocessLang?: string
  58. preprocessOptions?: any
  59. /**
  60. * In some cases, compiler-sfc may not be inside the project root (e.g. when
  61. * linked or globally installed). In such cases a custom `require` can be
  62. * passed to correctly resolve the preprocessors.
  63. */
  64. preprocessCustomRequire?: (id: string) => any
  65. /**
  66. * Configure what tags/attributes to transform into asset url imports,
  67. * or disable the transform altogether with `false`.
  68. */
  69. transformAssetUrls?: AssetURLOptions | AssetURLTagConfig | boolean
  70. }
  71. interface PreProcessor {
  72. render(
  73. source: string,
  74. options: any,
  75. cb: (err: Error | null, res: string) => void,
  76. ): void
  77. }
  78. function preprocess(
  79. { source, filename, preprocessOptions }: SFCTemplateCompileOptions,
  80. preprocessor: PreProcessor,
  81. ): string {
  82. // Consolidate exposes a callback based API, but the callback is in fact
  83. // called synchronously for most templating engines. In our case, we have to
  84. // expose a synchronous API so that it is usable in Jest transforms (which
  85. // have to be sync because they are applied via Node.js require hooks)
  86. let res: string = ''
  87. let err: Error | null = null
  88. preprocessor.render(
  89. source,
  90. { filename, ...preprocessOptions },
  91. (_err, _res) => {
  92. if (_err) err = _err
  93. res = _res
  94. },
  95. )
  96. if (err) throw err
  97. return res
  98. }
  99. export function compileTemplate(
  100. options: SFCTemplateCompileOptions,
  101. ): SFCTemplateCompileResults {
  102. const { preprocessLang, preprocessCustomRequire } = options
  103. if (
  104. (__ESM_BROWSER__ || __GLOBAL__) &&
  105. preprocessLang &&
  106. !preprocessCustomRequire
  107. ) {
  108. throw new Error(
  109. `[@vue/compiler-sfc] Template preprocessing in the browser build must ` +
  110. `provide the \`preprocessCustomRequire\` option to return the in-browser ` +
  111. `version of the preprocessor in the shape of { render(): string }.`,
  112. )
  113. }
  114. const preprocessor = preprocessLang
  115. ? preprocessCustomRequire
  116. ? preprocessCustomRequire(preprocessLang)
  117. : __ESM_BROWSER__
  118. ? undefined
  119. : consolidate[preprocessLang as keyof typeof consolidate]
  120. : false
  121. if (preprocessor) {
  122. try {
  123. return doCompileTemplate({
  124. ...options,
  125. source: preprocess(options, preprocessor),
  126. ast: undefined, // invalidate AST if template goes through preprocessor
  127. })
  128. } catch (e: any) {
  129. return {
  130. code: `export default function render() {}`,
  131. source: options.source,
  132. tips: [],
  133. errors: [e],
  134. }
  135. }
  136. } else if (preprocessLang) {
  137. return {
  138. code: `export default function render() {}`,
  139. source: options.source,
  140. tips: [
  141. `Component ${options.filename} uses lang ${preprocessLang} for template. Please install the language preprocessor.`,
  142. ],
  143. errors: [
  144. `Component ${options.filename} uses lang ${preprocessLang} for template, however it is not installed.`,
  145. ],
  146. }
  147. } else {
  148. return doCompileTemplate(options)
  149. }
  150. }
  151. function doCompileTemplate({
  152. filename,
  153. id,
  154. scoped,
  155. slotted,
  156. inMap,
  157. source,
  158. ast: inAST,
  159. ssr = false,
  160. ssrCssVars,
  161. isProd = false,
  162. compiler,
  163. compilerOptions = {},
  164. transformAssetUrls,
  165. }: SFCTemplateCompileOptions): SFCTemplateCompileResults {
  166. const errors: CompilerError[] = []
  167. const warnings: CompilerError[] = []
  168. let nodeTransforms: NodeTransform[] = []
  169. if (isObject(transformAssetUrls)) {
  170. const assetOptions = normalizeOptions(transformAssetUrls)
  171. nodeTransforms = [
  172. createAssetUrlTransformWithOptions(assetOptions),
  173. createSrcsetTransformWithOptions(assetOptions),
  174. ]
  175. } else if (transformAssetUrls !== false) {
  176. nodeTransforms = [transformAssetUrl, transformSrcset]
  177. }
  178. if (ssr && !ssrCssVars) {
  179. warnOnce(
  180. `compileTemplate is called with \`ssr: true\` but no ` +
  181. `corresponding \`cssVars\` option.`,
  182. )
  183. }
  184. if (!id) {
  185. warnOnce(`compileTemplate now requires the \`id\` option.`)
  186. id = ''
  187. }
  188. const shortId = id.replace(/^data-v-/, '')
  189. const longId = `data-v-${shortId}`
  190. const defaultCompiler = ssr ? (CompilerSSR as TemplateCompiler) : CompilerDOM
  191. compiler = compiler || defaultCompiler
  192. if (compiler !== defaultCompiler) {
  193. // user using custom compiler, this means we cannot reuse the AST from
  194. // the descriptor as they might be different.
  195. inAST = undefined
  196. }
  197. if (inAST?.transformed) {
  198. // If input AST has already been transformed, then it cannot be reused.
  199. // We need to parse a fresh one. Can't just use `source` here since we need
  200. // the AST location info to be relative to the entire SFC.
  201. const newAST = (ssr ? CompilerDOM : compiler).parse(inAST.source, {
  202. prefixIdentifiers: true,
  203. ...compilerOptions,
  204. parseMode: 'sfc',
  205. onError: e => errors.push(e),
  206. })
  207. const template = newAST.children.find(
  208. node => node.type === NodeTypes.ELEMENT && node.tag === 'template',
  209. ) as ElementNode
  210. inAST = createRoot(template.children, inAST.source)
  211. }
  212. let { code, ast, preamble, map } = compiler.compile(inAST || source, {
  213. mode: 'module',
  214. prefixIdentifiers: true,
  215. hoistStatic: true,
  216. cacheHandlers: true,
  217. ssrCssVars:
  218. ssr && ssrCssVars && ssrCssVars.length
  219. ? genCssVarsFromList(ssrCssVars, shortId, isProd, true)
  220. : '',
  221. scopeId: scoped ? longId : undefined,
  222. slotted,
  223. sourceMap: true,
  224. ...compilerOptions,
  225. hmr: !isProd,
  226. nodeTransforms: nodeTransforms.concat(compilerOptions.nodeTransforms || []),
  227. filename,
  228. onError: e => errors.push(e),
  229. onWarn: w => warnings.push(w),
  230. })
  231. // inMap should be the map produced by ./parse.ts which is a simple line-only
  232. // mapping. If it is present, we need to adjust the final map and errors to
  233. // reflect the original line numbers.
  234. if (inMap && !inAST) {
  235. if (map) {
  236. map = mapLines(inMap, map)
  237. }
  238. if (errors.length) {
  239. patchErrors(errors, source, inMap)
  240. }
  241. }
  242. const tips = warnings.map(w => {
  243. let msg = w.message
  244. if (w.loc) {
  245. msg += `\n${generateCodeFrame(
  246. inAST?.source || source,
  247. w.loc.start.offset,
  248. w.loc.end.offset,
  249. )}`
  250. }
  251. return msg
  252. })
  253. return { code, ast, preamble, source, errors, tips, map }
  254. }
  255. function mapLines(oldMap: RawSourceMap, newMap: RawSourceMap): RawSourceMap {
  256. if (!oldMap) return newMap
  257. if (!newMap) return oldMap
  258. const oldMapConsumer = new SourceMapConsumer(oldMap)
  259. const newMapConsumer = new SourceMapConsumer(newMap)
  260. const mergedMapGenerator = new SourceMapGenerator()
  261. newMapConsumer.eachMapping(m => {
  262. if (m.originalLine == null) {
  263. return
  264. }
  265. const origPosInOldMap = oldMapConsumer.originalPositionFor({
  266. line: m.originalLine,
  267. column: m.originalColumn!,
  268. })
  269. if (origPosInOldMap.source == null) {
  270. return
  271. }
  272. mergedMapGenerator.addMapping({
  273. generated: {
  274. line: m.generatedLine,
  275. column: m.generatedColumn,
  276. },
  277. original: {
  278. line: origPosInOldMap.line, // map line
  279. // use current column, since the oldMap produced by @vue/compiler-sfc
  280. // does not
  281. column: m.originalColumn!,
  282. },
  283. source: origPosInOldMap.source,
  284. name: origPosInOldMap.name,
  285. })
  286. })
  287. // source-map's type definition is incomplete
  288. const generator = mergedMapGenerator as any
  289. ;(oldMapConsumer as any).sources.forEach((sourceFile: string) => {
  290. generator._sources.add(sourceFile)
  291. const sourceContent = oldMapConsumer.sourceContentFor(sourceFile)
  292. if (sourceContent != null) {
  293. mergedMapGenerator.setSourceContent(sourceFile, sourceContent)
  294. }
  295. })
  296. generator._sourceRoot = oldMap.sourceRoot
  297. generator._file = oldMap.file
  298. return generator.toJSON()
  299. }
  300. function patchErrors(
  301. errors: CompilerError[],
  302. source: string,
  303. inMap: RawSourceMap,
  304. ) {
  305. const originalSource = inMap.sourcesContent![0]
  306. const offset = originalSource.indexOf(source)
  307. const lineOffset = originalSource.slice(0, offset).split(/\r?\n/).length - 1
  308. errors.forEach(err => {
  309. if (err.loc) {
  310. err.loc.start.line += lineOffset
  311. err.loc.start.offset += offset
  312. if (err.loc.end !== err.loc.start) {
  313. err.loc.end.line += lineOffset
  314. err.loc.end.offset += offset
  315. }
  316. }
  317. })
  318. }