parse.ts 13 KB

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