compileTemplate.ts 9.9 KB

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