parse.ts 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412
  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 { parseCssVars } from './cssVars'
  13. import { createCache } from './cache'
  14. export interface SFCParseOptions {
  15. filename?: string
  16. sourceMap?: boolean
  17. sourceRoot?: string
  18. pad?: boolean | 'line' | 'space'
  19. ignoreEmpty?: boolean
  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. /**
  40. * import('\@babel/types').Statement
  41. */
  42. scriptAst?: any[]
  43. /**
  44. * import('\@babel/types').Statement
  45. */
  46. scriptSetupAst?: any[]
  47. }
  48. export interface SFCStyleBlock extends SFCBlock {
  49. type: 'style'
  50. scoped?: boolean
  51. module?: string | boolean
  52. }
  53. export interface SFCDescriptor {
  54. filename: string
  55. source: string
  56. template: SFCTemplateBlock | null
  57. script: SFCScriptBlock | null
  58. scriptSetup: SFCScriptBlock | null
  59. styles: SFCStyleBlock[]
  60. customBlocks: SFCBlock[]
  61. cssVars: string[]
  62. // whether the SFC uses :slotted() modifier.
  63. // this is used as a compiler optimization hint.
  64. slotted: boolean
  65. }
  66. export interface SFCParseResult {
  67. descriptor: SFCDescriptor
  68. errors: (CompilerError | SyntaxError)[]
  69. }
  70. const sourceToSFC = createCache<SFCParseResult>()
  71. export function parse(
  72. source: string,
  73. {
  74. sourceMap = true,
  75. filename = 'anonymous.vue',
  76. sourceRoot = '',
  77. pad = false,
  78. ignoreEmpty = true,
  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. // we only want to keep the nodes that are not empty (when the tag is not a template)
  135. if (
  136. ignoreEmpty &&
  137. node.tag !== 'template' &&
  138. isEmpty(node) &&
  139. !hasSrc(node)
  140. ) {
  141. return
  142. }
  143. switch (node.tag) {
  144. case 'template':
  145. if (!descriptor.template) {
  146. const templateBlock = (descriptor.template = createBlock(
  147. node,
  148. source,
  149. false
  150. ) as SFCTemplateBlock)
  151. templateBlock.ast = node
  152. // warn against 2.x <template functional>
  153. if (templateBlock.attrs.functional) {
  154. const err = new SyntaxError(
  155. `<template functional> is no longer supported in Vue 3, since ` +
  156. `functional components no longer have significant performance ` +
  157. `difference from stateful ones. Just use a normal <template> ` +
  158. `instead.`
  159. ) as CompilerError
  160. err.loc = node.props.find(p => p.name === 'functional')!.loc
  161. errors.push(err)
  162. }
  163. } else {
  164. errors.push(createDuplicateBlockError(node))
  165. }
  166. break
  167. case 'script':
  168. const scriptBlock = createBlock(node, source, pad) as SFCScriptBlock
  169. const isSetup = !!scriptBlock.attrs.setup
  170. if (isSetup && !descriptor.scriptSetup) {
  171. descriptor.scriptSetup = scriptBlock
  172. break
  173. }
  174. if (!isSetup && !descriptor.script) {
  175. descriptor.script = scriptBlock
  176. break
  177. }
  178. errors.push(createDuplicateBlockError(node, isSetup))
  179. break
  180. case 'style':
  181. const styleBlock = createBlock(node, source, pad) as SFCStyleBlock
  182. if (styleBlock.attrs.vars) {
  183. errors.push(
  184. new SyntaxError(
  185. `<style vars> has been replaced by a new proposal: ` +
  186. `https://github.com/vuejs/rfcs/pull/231`
  187. )
  188. )
  189. }
  190. descriptor.styles.push(styleBlock)
  191. break
  192. default:
  193. descriptor.customBlocks.push(createBlock(node, source, pad))
  194. break
  195. }
  196. })
  197. if (descriptor.scriptSetup) {
  198. if (descriptor.scriptSetup.src) {
  199. errors.push(
  200. new SyntaxError(
  201. `<script setup> cannot use the "src" attribute because ` +
  202. `its syntax will be ambiguous outside of the component.`
  203. )
  204. )
  205. descriptor.scriptSetup = null
  206. }
  207. if (descriptor.script && descriptor.script.src) {
  208. errors.push(
  209. new SyntaxError(
  210. `<script> cannot use the "src" attribute when <script setup> is ` +
  211. `also present because they must be processed together.`
  212. )
  213. )
  214. descriptor.script = null
  215. }
  216. }
  217. if (sourceMap) {
  218. const genMap = (block: SFCBlock | null) => {
  219. if (block && !block.src) {
  220. block.map = generateSourceMap(
  221. filename,
  222. source,
  223. block.content,
  224. sourceRoot,
  225. !pad || block.type === 'template' ? block.loc.start.line - 1 : 0
  226. )
  227. }
  228. }
  229. genMap(descriptor.template)
  230. genMap(descriptor.script)
  231. descriptor.styles.forEach(genMap)
  232. descriptor.customBlocks.forEach(genMap)
  233. }
  234. // parse CSS vars
  235. descriptor.cssVars = parseCssVars(descriptor)
  236. // check if the SFC uses :slotted
  237. const slottedRE = /(?:::v-|:)slotted\(/
  238. descriptor.slotted = descriptor.styles.some(
  239. s => s.scoped && slottedRE.test(s.content)
  240. )
  241. const result = {
  242. descriptor,
  243. errors
  244. }
  245. sourceToSFC.set(sourceKey, result)
  246. return result
  247. }
  248. function createDuplicateBlockError(
  249. node: ElementNode,
  250. isScriptSetup = false
  251. ): CompilerError {
  252. const err = new SyntaxError(
  253. `Single file component can contain only one <${node.tag}${
  254. isScriptSetup ? ` setup` : ``
  255. }> element`
  256. ) as CompilerError
  257. err.loc = node.loc
  258. return err
  259. }
  260. function createBlock(
  261. node: ElementNode,
  262. source: string,
  263. pad: SFCParseOptions['pad']
  264. ): SFCBlock {
  265. const type = node.tag
  266. let { start, end } = node.loc
  267. let content = ''
  268. if (node.children.length) {
  269. start = node.children[0].loc.start
  270. end = node.children[node.children.length - 1].loc.end
  271. content = source.slice(start.offset, end.offset)
  272. } else {
  273. const offset = node.loc.source.indexOf(`</`)
  274. if (offset > -1) {
  275. start = {
  276. line: start.line,
  277. column: start.column + offset,
  278. offset: start.offset + offset
  279. }
  280. }
  281. end = { ...start }
  282. }
  283. const loc = {
  284. source: content,
  285. start,
  286. end
  287. }
  288. const attrs: Record<string, string | true> = {}
  289. const block: SFCBlock = {
  290. type,
  291. content,
  292. loc,
  293. attrs
  294. }
  295. if (pad) {
  296. block.content = padContent(source, block, pad) + block.content
  297. }
  298. node.props.forEach(p => {
  299. if (p.type === NodeTypes.ATTRIBUTE) {
  300. attrs[p.name] = p.value ? p.value.content || true : true
  301. if (p.name === 'lang') {
  302. block.lang = p.value && p.value.content
  303. } else if (p.name === 'src') {
  304. block.src = p.value && p.value.content
  305. } else if (type === 'style') {
  306. if (p.name === 'scoped') {
  307. ;(block as SFCStyleBlock).scoped = true
  308. } else if (p.name === 'module') {
  309. ;(block as SFCStyleBlock).module = attrs[p.name]
  310. }
  311. } else if (type === 'script' && p.name === 'setup') {
  312. ;(block as SFCScriptBlock).setup = attrs.setup
  313. }
  314. }
  315. })
  316. return block
  317. }
  318. const splitRE = /\r?\n/g
  319. const emptyRE = /^(?:\/\/)?\s*$/
  320. const replaceRE = /./g
  321. function generateSourceMap(
  322. filename: string,
  323. source: string,
  324. generated: string,
  325. sourceRoot: string,
  326. lineOffset: number
  327. ): RawSourceMap {
  328. const map = new SourceMapGenerator({
  329. file: filename.replace(/\\/g, '/'),
  330. sourceRoot: sourceRoot.replace(/\\/g, '/')
  331. })
  332. map.setSourceContent(filename, source)
  333. generated.split(splitRE).forEach((line, index) => {
  334. if (!emptyRE.test(line)) {
  335. const originalLine = index + 1 + lineOffset
  336. const generatedLine = index + 1
  337. for (let i = 0; i < line.length; i++) {
  338. if (!/\s/.test(line[i])) {
  339. map.addMapping({
  340. source: filename,
  341. original: {
  342. line: originalLine,
  343. column: i
  344. },
  345. generated: {
  346. line: generatedLine,
  347. column: i
  348. }
  349. })
  350. }
  351. }
  352. }
  353. })
  354. return JSON.parse(map.toString())
  355. }
  356. function padContent(
  357. content: string,
  358. block: SFCBlock,
  359. pad: SFCParseOptions['pad']
  360. ): string {
  361. content = content.slice(0, block.loc.start.offset)
  362. if (pad === 'space') {
  363. return content.replace(replaceRE, ' ')
  364. } else {
  365. const offset = content.split(splitRE).length
  366. const padChar = block.type === 'script' && !block.lang ? '//\n' : '\n'
  367. return Array(offset).join(padChar)
  368. }
  369. }
  370. function hasSrc(node: ElementNode) {
  371. return node.props.some(p => {
  372. if (p.type !== NodeTypes.ATTRIBUTE) {
  373. return false
  374. }
  375. return p.name === 'src'
  376. })
  377. }
  378. /**
  379. * Returns true if the node has no children
  380. * once the empty text nodes (trimmed content) have been filtered out.
  381. */
  382. function isEmpty(node: ElementNode) {
  383. return (
  384. node.children.filter(
  385. child => child.type !== NodeTypes.TEXT || child.content.trim() !== ''
  386. ).length === 0
  387. )
  388. }