parse.ts 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468
  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-js'
  11. import { TemplateCompiler } from './compileTemplate'
  12. import { parseCssVars } from './style/cssVars'
  13. import { createCache } from './cache'
  14. import { ImportBinding } from './compileScript'
  15. import { isImportUsed } from './script/importUsageCheck'
  16. export const DEFAULT_FILENAME = 'anonymous.vue'
  17. export interface SFCParseOptions {
  18. filename?: string
  19. sourceMap?: boolean
  20. sourceRoot?: string
  21. pad?: boolean | 'line' | 'space'
  22. ignoreEmpty?: boolean
  23. compiler?: TemplateCompiler
  24. }
  25. export interface SFCBlock {
  26. type: string
  27. content: string
  28. attrs: Record<string, string | true>
  29. loc: SourceLocation
  30. map?: RawSourceMap
  31. lang?: string
  32. src?: string
  33. }
  34. export interface SFCTemplateBlock extends SFCBlock {
  35. type: 'template'
  36. ast: ElementNode
  37. }
  38. export interface SFCScriptBlock extends SFCBlock {
  39. type: 'script'
  40. setup?: string | boolean
  41. bindings?: BindingMetadata
  42. imports?: Record<string, ImportBinding>
  43. scriptAst?: import('@babel/types').Statement[]
  44. scriptSetupAst?: import('@babel/types').Statement[]
  45. warnings?: string[]
  46. /**
  47. * Fully resolved dependency file paths (unix slashes) with imported types
  48. * used in macros, used for HMR cache busting in @vitejs/plugin-vue and
  49. * vue-loader.
  50. */
  51. deps?: string[]
  52. }
  53. export interface SFCStyleBlock extends SFCBlock {
  54. type: 'style'
  55. scoped?: boolean
  56. module?: string | boolean
  57. }
  58. export interface SFCDescriptor {
  59. filename: string
  60. source: string
  61. template: SFCTemplateBlock | null
  62. script: SFCScriptBlock | null
  63. scriptSetup: SFCScriptBlock | null
  64. styles: SFCStyleBlock[]
  65. customBlocks: SFCBlock[]
  66. cssVars: string[]
  67. /**
  68. * whether the SFC uses :slotted() modifier.
  69. * this is used as a compiler optimization hint.
  70. */
  71. slotted: boolean
  72. /**
  73. * compare with an existing descriptor to determine whether HMR should perform
  74. * a reload vs. re-render.
  75. *
  76. * Note: this comparison assumes the prev/next script are already identical,
  77. * and only checks the special case where <script setup lang="ts"> unused import
  78. * pruning result changes due to template changes.
  79. */
  80. shouldForceReload: (prevImports: Record<string, ImportBinding>) => boolean
  81. }
  82. export interface SFCParseResult {
  83. descriptor: SFCDescriptor
  84. errors: (CompilerError | SyntaxError)[]
  85. }
  86. export const parseCache = createCache<SFCParseResult>()
  87. export function parse(
  88. source: string,
  89. {
  90. sourceMap = true,
  91. filename = DEFAULT_FILENAME,
  92. sourceRoot = '',
  93. pad = false,
  94. ignoreEmpty = true,
  95. compiler = CompilerDOM
  96. }: SFCParseOptions = {}
  97. ): SFCParseResult {
  98. const sourceKey =
  99. source + sourceMap + filename + sourceRoot + pad + compiler.parse
  100. const cache = parseCache.get(sourceKey)
  101. if (cache) {
  102. return cache
  103. }
  104. const descriptor: SFCDescriptor = {
  105. filename,
  106. source,
  107. template: null,
  108. script: null,
  109. scriptSetup: null,
  110. styles: [],
  111. customBlocks: [],
  112. cssVars: [],
  113. slotted: false,
  114. shouldForceReload: prevImports => hmrShouldReload(prevImports, descriptor)
  115. }
  116. const errors: (CompilerError | SyntaxError)[] = []
  117. const ast = compiler.parse(source, {
  118. // there are no components at SFC parsing level
  119. isNativeTag: () => true,
  120. // preserve all whitespaces
  121. isPreTag: () => true,
  122. getTextMode: ({ tag, props }, parent) => {
  123. // all top level elements except <template> are parsed as raw text
  124. // containers
  125. if (
  126. (!parent && tag !== 'template') ||
  127. // <template lang="xxx"> should also be treated as raw text
  128. (tag === 'template' &&
  129. props.some(
  130. p =>
  131. p.type === NodeTypes.ATTRIBUTE &&
  132. p.name === 'lang' &&
  133. p.value &&
  134. p.value.content &&
  135. p.value.content !== 'html'
  136. ))
  137. ) {
  138. return TextModes.RAWTEXT
  139. } else {
  140. return TextModes.DATA
  141. }
  142. },
  143. onError: e => {
  144. errors.push(e)
  145. }
  146. })
  147. ast.children.forEach(node => {
  148. if (node.type !== NodeTypes.ELEMENT) {
  149. return
  150. }
  151. // we only want to keep the nodes that are not empty (when the tag is not a template)
  152. if (
  153. ignoreEmpty &&
  154. node.tag !== 'template' &&
  155. isEmpty(node) &&
  156. !hasSrc(node)
  157. ) {
  158. return
  159. }
  160. switch (node.tag) {
  161. case 'template':
  162. if (!descriptor.template) {
  163. const templateBlock = (descriptor.template = createBlock(
  164. node,
  165. source,
  166. false
  167. ) as SFCTemplateBlock)
  168. templateBlock.ast = node
  169. // warn against 2.x <template functional>
  170. if (templateBlock.attrs.functional) {
  171. const err = new SyntaxError(
  172. `<template functional> is no longer supported in Vue 3, since ` +
  173. `functional components no longer have significant performance ` +
  174. `difference from stateful ones. Just use a normal <template> ` +
  175. `instead.`
  176. ) as CompilerError
  177. err.loc = node.props.find(p => p.name === 'functional')!.loc
  178. errors.push(err)
  179. }
  180. } else {
  181. errors.push(createDuplicateBlockError(node))
  182. }
  183. break
  184. case 'script':
  185. const scriptBlock = createBlock(node, source, pad) as SFCScriptBlock
  186. const isSetup = !!scriptBlock.attrs.setup
  187. if (isSetup && !descriptor.scriptSetup) {
  188. descriptor.scriptSetup = scriptBlock
  189. break
  190. }
  191. if (!isSetup && !descriptor.script) {
  192. descriptor.script = scriptBlock
  193. break
  194. }
  195. errors.push(createDuplicateBlockError(node, isSetup))
  196. break
  197. case 'style':
  198. const styleBlock = createBlock(node, source, pad) as SFCStyleBlock
  199. if (styleBlock.attrs.vars) {
  200. errors.push(
  201. new SyntaxError(
  202. `<style vars> has been replaced by a new proposal: ` +
  203. `https://github.com/vuejs/rfcs/pull/231`
  204. )
  205. )
  206. }
  207. descriptor.styles.push(styleBlock)
  208. break
  209. default:
  210. descriptor.customBlocks.push(createBlock(node, source, pad))
  211. break
  212. }
  213. })
  214. if (!descriptor.template && !descriptor.script && !descriptor.scriptSetup) {
  215. errors.push(
  216. new SyntaxError(
  217. `At least one <template> or <script> is required in a single file component.`
  218. )
  219. )
  220. }
  221. if (descriptor.scriptSetup) {
  222. if (descriptor.scriptSetup.src) {
  223. errors.push(
  224. new SyntaxError(
  225. `<script setup> cannot use the "src" attribute because ` +
  226. `its syntax will be ambiguous outside of the component.`
  227. )
  228. )
  229. descriptor.scriptSetup = null
  230. }
  231. if (descriptor.script && descriptor.script.src) {
  232. errors.push(
  233. new SyntaxError(
  234. `<script> cannot use the "src" attribute when <script setup> is ` +
  235. `also present because they must be processed together.`
  236. )
  237. )
  238. descriptor.script = null
  239. }
  240. }
  241. if (sourceMap) {
  242. const genMap = (block: SFCBlock | null) => {
  243. if (block && !block.src) {
  244. block.map = generateSourceMap(
  245. filename,
  246. source,
  247. block.content,
  248. sourceRoot,
  249. !pad || block.type === 'template' ? block.loc.start.line - 1 : 0
  250. )
  251. }
  252. }
  253. genMap(descriptor.template)
  254. genMap(descriptor.script)
  255. descriptor.styles.forEach(genMap)
  256. descriptor.customBlocks.forEach(genMap)
  257. }
  258. // parse CSS vars
  259. descriptor.cssVars = parseCssVars(descriptor)
  260. // check if the SFC uses :slotted
  261. const slottedRE = /(?:::v-|:)slotted\(/
  262. descriptor.slotted = descriptor.styles.some(
  263. s => s.scoped && slottedRE.test(s.content)
  264. )
  265. const result = {
  266. descriptor,
  267. errors
  268. }
  269. parseCache.set(sourceKey, result)
  270. return result
  271. }
  272. function createDuplicateBlockError(
  273. node: ElementNode,
  274. isScriptSetup = false
  275. ): CompilerError {
  276. const err = new SyntaxError(
  277. `Single file component can contain only one <${node.tag}${
  278. isScriptSetup ? ` setup` : ``
  279. }> element`
  280. ) as CompilerError
  281. err.loc = node.loc
  282. return err
  283. }
  284. function createBlock(
  285. node: ElementNode,
  286. source: string,
  287. pad: SFCParseOptions['pad']
  288. ): SFCBlock {
  289. const type = node.tag
  290. let { start, end } = node.loc
  291. let content = ''
  292. if (node.children.length) {
  293. start = node.children[0].loc.start
  294. end = node.children[node.children.length - 1].loc.end
  295. content = source.slice(start.offset, end.offset)
  296. } else {
  297. const offset = node.loc.source.indexOf(`</`)
  298. if (offset > -1) {
  299. start = {
  300. line: start.line,
  301. column: start.column + offset,
  302. offset: start.offset + offset
  303. }
  304. }
  305. end = { ...start }
  306. }
  307. const loc = {
  308. source: content,
  309. start,
  310. end
  311. }
  312. const attrs: Record<string, string | true> = {}
  313. const block: SFCBlock = {
  314. type,
  315. content,
  316. loc,
  317. attrs
  318. }
  319. if (pad) {
  320. block.content = padContent(source, block, pad) + block.content
  321. }
  322. node.props.forEach(p => {
  323. if (p.type === NodeTypes.ATTRIBUTE) {
  324. attrs[p.name] = p.value ? p.value.content || true : true
  325. if (p.name === 'lang') {
  326. block.lang = p.value && p.value.content
  327. } else if (p.name === 'src') {
  328. block.src = p.value && p.value.content
  329. } else if (type === 'style') {
  330. if (p.name === 'scoped') {
  331. ;(block as SFCStyleBlock).scoped = true
  332. } else if (p.name === 'module') {
  333. ;(block as SFCStyleBlock).module = attrs[p.name]
  334. }
  335. } else if (type === 'script' && p.name === 'setup') {
  336. ;(block as SFCScriptBlock).setup = attrs.setup
  337. }
  338. }
  339. })
  340. return block
  341. }
  342. const splitRE = /\r?\n/g
  343. const emptyRE = /^(?:\/\/)?\s*$/
  344. const replaceRE = /./g
  345. function generateSourceMap(
  346. filename: string,
  347. source: string,
  348. generated: string,
  349. sourceRoot: string,
  350. lineOffset: number
  351. ): RawSourceMap {
  352. const map = new SourceMapGenerator({
  353. file: filename.replace(/\\/g, '/'),
  354. sourceRoot: sourceRoot.replace(/\\/g, '/')
  355. })
  356. map.setSourceContent(filename, source)
  357. generated.split(splitRE).forEach((line, index) => {
  358. if (!emptyRE.test(line)) {
  359. const originalLine = index + 1 + lineOffset
  360. const generatedLine = index + 1
  361. for (let i = 0; i < line.length; i++) {
  362. if (!/\s/.test(line[i])) {
  363. map.addMapping({
  364. source: filename,
  365. original: {
  366. line: originalLine,
  367. column: i
  368. },
  369. generated: {
  370. line: generatedLine,
  371. column: i
  372. }
  373. })
  374. }
  375. }
  376. }
  377. })
  378. return JSON.parse(map.toString())
  379. }
  380. function padContent(
  381. content: string,
  382. block: SFCBlock,
  383. pad: SFCParseOptions['pad']
  384. ): string {
  385. content = content.slice(0, block.loc.start.offset)
  386. if (pad === 'space') {
  387. return content.replace(replaceRE, ' ')
  388. } else {
  389. const offset = content.split(splitRE).length
  390. const padChar = block.type === 'script' && !block.lang ? '//\n' : '\n'
  391. return Array(offset).join(padChar)
  392. }
  393. }
  394. function hasSrc(node: ElementNode) {
  395. return node.props.some(p => {
  396. if (p.type !== NodeTypes.ATTRIBUTE) {
  397. return false
  398. }
  399. return p.name === 'src'
  400. })
  401. }
  402. /**
  403. * Returns true if the node has no children
  404. * once the empty text nodes (trimmed content) have been filtered out.
  405. */
  406. function isEmpty(node: ElementNode) {
  407. for (let i = 0; i < node.children.length; i++) {
  408. const child = node.children[i]
  409. if (child.type !== NodeTypes.TEXT || child.content.trim() !== '') {
  410. return false
  411. }
  412. }
  413. return true
  414. }
  415. /**
  416. * Note: this comparison assumes the prev/next script are already identical,
  417. * and only checks the special case where <script setup lang="ts"> unused import
  418. * pruning result changes due to template changes.
  419. */
  420. export function hmrShouldReload(
  421. prevImports: Record<string, ImportBinding>,
  422. next: SFCDescriptor
  423. ): boolean {
  424. if (
  425. !next.scriptSetup ||
  426. (next.scriptSetup.lang !== 'ts' && next.scriptSetup.lang !== 'tsx')
  427. ) {
  428. return false
  429. }
  430. // for each previous import, check if its used status remain the same based on
  431. // the next descriptor's template
  432. for (const key in prevImports) {
  433. // if an import was previous unused, but now is used, we need to force
  434. // reload so that the script now includes that import.
  435. if (!prevImports[key].isUsedInTemplate && isImportUsed(key, next)) {
  436. return true
  437. }
  438. }
  439. return false
  440. }