parse.ts 9.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388
  1. import {
  2. NodeTypes,
  3. ElementNode,
  4. SourceLocation,
  5. CompilerError,
  6. TextModes,
  7. BindingMetadata
  8. } from '@vue/compiler-core'
  9. import * as CompilerDOM from '@vue/compiler-dom'
  10. import { RawSourceMap, SourceMapGenerator } from 'source-map'
  11. import { TemplateCompiler } from './compileTemplate'
  12. import { Statement } from '@babel/types'
  13. import { parseCssVars } from './cssVars'
  14. import { warnExperimental } from './warn'
  15. export interface SFCParseOptions {
  16. filename?: string
  17. sourceMap?: boolean
  18. sourceRoot?: string
  19. pad?: boolean | 'line' | 'space'
  20. compiler?: TemplateCompiler
  21. }
  22. export interface SFCBlock {
  23. type: string
  24. content: string
  25. attrs: Record<string, string | true>
  26. loc: SourceLocation
  27. map?: RawSourceMap
  28. lang?: string
  29. src?: string
  30. }
  31. export interface SFCTemplateBlock extends SFCBlock {
  32. type: 'template'
  33. ast: ElementNode
  34. }
  35. export interface SFCScriptBlock extends SFCBlock {
  36. type: 'script'
  37. setup?: string | boolean
  38. bindings?: BindingMetadata
  39. scriptAst?: Statement[]
  40. scriptSetupAst?: Statement[]
  41. }
  42. export interface SFCStyleBlock extends SFCBlock {
  43. type: 'style'
  44. scoped?: boolean
  45. module?: string | boolean
  46. }
  47. export interface SFCDescriptor {
  48. filename: string
  49. source: string
  50. template: SFCTemplateBlock | null
  51. script: SFCScriptBlock | null
  52. scriptSetup: SFCScriptBlock | null
  53. styles: SFCStyleBlock[]
  54. customBlocks: SFCBlock[]
  55. cssVars: string[]
  56. // whether the SFC uses :slotted() modifier.
  57. // this is used as a compiler optimization hint.
  58. slotted: boolean
  59. }
  60. export interface SFCParseResult {
  61. descriptor: SFCDescriptor
  62. errors: (CompilerError | SyntaxError)[]
  63. }
  64. const SFC_CACHE_MAX_SIZE = 500
  65. const sourceToSFC =
  66. __GLOBAL__ || __ESM_BROWSER__
  67. ? new Map<string, SFCParseResult>()
  68. : (new (require('lru-cache'))(SFC_CACHE_MAX_SIZE) as Map<
  69. string,
  70. SFCParseResult
  71. >)
  72. export function parse(
  73. source: string,
  74. {
  75. sourceMap = true,
  76. filename = 'anonymous.vue',
  77. sourceRoot = '',
  78. pad = false,
  79. compiler = CompilerDOM
  80. }: SFCParseOptions = {}
  81. ): SFCParseResult {
  82. const sourceKey =
  83. source + sourceMap + filename + sourceRoot + pad + compiler.parse
  84. const cache = sourceToSFC.get(sourceKey)
  85. if (cache) {
  86. return cache
  87. }
  88. const descriptor: SFCDescriptor = {
  89. filename,
  90. source,
  91. template: null,
  92. script: null,
  93. scriptSetup: null,
  94. styles: [],
  95. customBlocks: [],
  96. cssVars: [],
  97. slotted: false
  98. }
  99. const errors: (CompilerError | SyntaxError)[] = []
  100. const ast = compiler.parse(source, {
  101. // there are no components at SFC parsing level
  102. isNativeTag: () => true,
  103. // preserve all whitespaces
  104. isPreTag: () => true,
  105. getTextMode: ({ tag, props }, parent) => {
  106. // all top level elements except <template> are parsed as raw text
  107. // containers
  108. if (
  109. (!parent && tag !== 'template') ||
  110. // <template lang="xxx"> should also be treated as raw text
  111. (tag === 'template' &&
  112. props.some(
  113. p =>
  114. p.type === NodeTypes.ATTRIBUTE &&
  115. p.name === 'lang' &&
  116. p.value &&
  117. p.value.content &&
  118. p.value.content !== 'html'
  119. ))
  120. ) {
  121. return TextModes.RAWTEXT
  122. } else {
  123. return TextModes.DATA
  124. }
  125. },
  126. onError: e => {
  127. errors.push(e)
  128. }
  129. })
  130. ast.children.forEach(node => {
  131. if (node.type !== NodeTypes.ELEMENT) {
  132. return
  133. }
  134. if (!node.children.length && !hasSrc(node) && node.tag !== 'template') {
  135. return
  136. }
  137. switch (node.tag) {
  138. case 'template':
  139. if (!descriptor.template) {
  140. const templateBlock = (descriptor.template = createBlock(
  141. node,
  142. source,
  143. false
  144. ) as SFCTemplateBlock)
  145. templateBlock.ast = node
  146. // warn against 2.x <template functional>
  147. if (templateBlock.attrs.functional) {
  148. const err = new SyntaxError(
  149. `<template functional> is no longer supported in Vue 3, since ` +
  150. `functional components no longer have significant performance ` +
  151. `difference from stateful ones. Just use a normal <template> ` +
  152. `instead.`
  153. ) as CompilerError
  154. err.loc = node.props.find(p => p.name === 'functional')!.loc
  155. errors.push(err)
  156. }
  157. } else {
  158. errors.push(createDuplicateBlockError(node))
  159. }
  160. break
  161. case 'script':
  162. const scriptBlock = createBlock(node, source, pad) as SFCScriptBlock
  163. const isSetup = !!scriptBlock.attrs.setup
  164. if (isSetup && !descriptor.scriptSetup) {
  165. descriptor.scriptSetup = scriptBlock
  166. break
  167. }
  168. if (!isSetup && !descriptor.script) {
  169. descriptor.script = scriptBlock
  170. break
  171. }
  172. errors.push(createDuplicateBlockError(node, isSetup))
  173. break
  174. case 'style':
  175. const styleBlock = createBlock(node, source, pad) as SFCStyleBlock
  176. if (styleBlock.attrs.vars) {
  177. errors.push(
  178. new SyntaxError(
  179. `<style vars> has been replaced by a new proposal: ` +
  180. `https://github.com/vuejs/rfcs/pull/231`
  181. )
  182. )
  183. }
  184. descriptor.styles.push(styleBlock)
  185. break
  186. default:
  187. descriptor.customBlocks.push(createBlock(node, source, pad))
  188. break
  189. }
  190. })
  191. if (descriptor.scriptSetup) {
  192. if (descriptor.scriptSetup.src) {
  193. errors.push(
  194. new SyntaxError(
  195. `<script setup> cannot use the "src" attribute because ` +
  196. `its syntax will be ambiguous outside of the component.`
  197. )
  198. )
  199. descriptor.scriptSetup = null
  200. }
  201. if (descriptor.script && descriptor.script.src) {
  202. errors.push(
  203. new SyntaxError(
  204. `<script> cannot use the "src" attribute when <script setup> is ` +
  205. `also present because they must be processed together.`
  206. )
  207. )
  208. descriptor.script = null
  209. }
  210. }
  211. if (sourceMap) {
  212. const genMap = (block: SFCBlock | null) => {
  213. if (block && !block.src) {
  214. block.map = generateSourceMap(
  215. filename,
  216. source,
  217. block.content,
  218. sourceRoot,
  219. !pad || block.type === 'template' ? block.loc.start.line - 1 : 0
  220. )
  221. }
  222. }
  223. genMap(descriptor.template)
  224. genMap(descriptor.script)
  225. descriptor.styles.forEach(genMap)
  226. descriptor.customBlocks.forEach(genMap)
  227. }
  228. // parse CSS vars
  229. descriptor.cssVars = parseCssVars(descriptor)
  230. if (descriptor.cssVars.length) {
  231. warnExperimental(`v-bind() CSS variable injection`, 231)
  232. }
  233. // check if the SFC uses :slotted
  234. const slottedRE = /(?:::v-|:)slotted\(/
  235. descriptor.slotted = descriptor.styles.some(
  236. s => s.scoped && slottedRE.test(s.content)
  237. )
  238. const result = {
  239. descriptor,
  240. errors
  241. }
  242. sourceToSFC.set(sourceKey, result)
  243. return result
  244. }
  245. function createDuplicateBlockError(
  246. node: ElementNode,
  247. isScriptSetup = false
  248. ): CompilerError {
  249. const err = new SyntaxError(
  250. `Single file component can contain only one <${node.tag}${
  251. isScriptSetup ? ` setup` : ``
  252. }> element`
  253. ) as CompilerError
  254. err.loc = node.loc
  255. return err
  256. }
  257. function createBlock(
  258. node: ElementNode,
  259. source: string,
  260. pad: SFCParseOptions['pad']
  261. ): SFCBlock {
  262. const type = node.tag
  263. let { start, end } = node.loc
  264. let content = ''
  265. if (node.children.length) {
  266. start = node.children[0].loc.start
  267. end = node.children[node.children.length - 1].loc.end
  268. content = source.slice(start.offset, end.offset)
  269. }
  270. const loc = {
  271. source: content,
  272. start,
  273. end
  274. }
  275. const attrs: Record<string, string | true> = {}
  276. const block: SFCBlock = {
  277. type,
  278. content,
  279. loc,
  280. attrs
  281. }
  282. if (pad) {
  283. block.content = padContent(source, block, pad) + block.content
  284. }
  285. node.props.forEach(p => {
  286. if (p.type === NodeTypes.ATTRIBUTE) {
  287. attrs[p.name] = p.value ? p.value.content || true : true
  288. if (p.name === 'lang') {
  289. block.lang = p.value && p.value.content
  290. } else if (p.name === 'src') {
  291. block.src = p.value && p.value.content
  292. } else if (type === 'style') {
  293. if (p.name === 'scoped') {
  294. ;(block as SFCStyleBlock).scoped = true
  295. } else if (p.name === 'module') {
  296. ;(block as SFCStyleBlock).module = attrs[p.name]
  297. }
  298. } else if (type === 'script' && p.name === 'setup') {
  299. ;(block as SFCScriptBlock).setup = attrs.setup
  300. }
  301. }
  302. })
  303. return block
  304. }
  305. const splitRE = /\r?\n/g
  306. const emptyRE = /^(?:\/\/)?\s*$/
  307. const replaceRE = /./g
  308. function generateSourceMap(
  309. filename: string,
  310. source: string,
  311. generated: string,
  312. sourceRoot: string,
  313. lineOffset: number
  314. ): RawSourceMap {
  315. const map = new SourceMapGenerator({
  316. file: filename.replace(/\\/g, '/'),
  317. sourceRoot: sourceRoot.replace(/\\/g, '/')
  318. })
  319. map.setSourceContent(filename, source)
  320. generated.split(splitRE).forEach((line, index) => {
  321. if (!emptyRE.test(line)) {
  322. const originalLine = index + 1 + lineOffset
  323. const generatedLine = index + 1
  324. for (let i = 0; i < line.length; i++) {
  325. if (!/\s/.test(line[i])) {
  326. map.addMapping({
  327. source: filename,
  328. original: {
  329. line: originalLine,
  330. column: i
  331. },
  332. generated: {
  333. line: generatedLine,
  334. column: i
  335. }
  336. })
  337. }
  338. }
  339. }
  340. })
  341. return JSON.parse(map.toString())
  342. }
  343. function padContent(
  344. content: string,
  345. block: SFCBlock,
  346. pad: SFCParseOptions['pad']
  347. ): string {
  348. content = content.slice(0, block.loc.start.offset)
  349. if (pad === 'space') {
  350. return content.replace(replaceRE, ' ')
  351. } else {
  352. const offset = content.split(splitRE).length
  353. const padChar = block.type === 'script' && !block.lang ? '//\n' : '\n'
  354. return Array(offset).join(padChar)
  355. }
  356. }
  357. function hasSrc(node: ElementNode) {
  358. return node.props.some(p => {
  359. if (p.type !== NodeTypes.ATTRIBUTE) {
  360. return false
  361. }
  362. return p.name === 'src'
  363. })
  364. }