parse.ts 13 KB

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