parse.ts 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490
  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. 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. !hasSrc(node)
  151. ) {
  152. return
  153. }
  154. switch (node.tag) {
  155. case 'template':
  156. if (!descriptor.template) {
  157. const templateBlock = (descriptor.template = createBlock(
  158. node,
  159. source,
  160. false,
  161. ) as SFCTemplateBlock)
  162. descriptor.vapor ||= !!templateBlock.attrs.vapor
  163. if (!templateBlock.attrs.src) {
  164. templateBlock.ast = createRoot(node.children, source)
  165. }
  166. // warn against 2.x <template functional>
  167. if (templateBlock.attrs.functional) {
  168. const err = new SyntaxError(
  169. `<template functional> is no longer supported in Vue 3, since ` +
  170. `functional components no longer have significant performance ` +
  171. `difference from stateful ones. Just use a normal <template> ` +
  172. `instead.`,
  173. ) as CompilerError
  174. err.loc = node.props.find(
  175. p => p.type === NodeTypes.ATTRIBUTE && p.name === 'functional',
  176. )!.loc
  177. errors.push(err)
  178. }
  179. } else {
  180. errors.push(createDuplicateBlockError(node))
  181. }
  182. break
  183. case 'script':
  184. const scriptBlock = createBlock(node, source, pad) as SFCScriptBlock
  185. descriptor.vapor ||= !!scriptBlock.attrs.vapor
  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. ${descriptor.filename}`,
  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. // dedent pug/jade templates
  242. let templateColumnOffset = 0
  243. if (
  244. descriptor.template &&
  245. (descriptor.template.lang === 'pug' || descriptor.template.lang === 'jade')
  246. ) {
  247. ;[descriptor.template.content, templateColumnOffset] = dedent(
  248. descriptor.template.content,
  249. )
  250. }
  251. if (sourceMap) {
  252. const genMap = (block: SFCBlock | null, columnOffset = 0) => {
  253. if (block && !block.src) {
  254. block.map = generateSourceMap(
  255. filename,
  256. source,
  257. block.content,
  258. sourceRoot,
  259. !pad || block.type === 'template' ? block.loc.start.line - 1 : 0,
  260. columnOffset,
  261. )
  262. }
  263. }
  264. genMap(descriptor.template, templateColumnOffset)
  265. genMap(descriptor.script)
  266. descriptor.styles.forEach(s => genMap(s))
  267. descriptor.customBlocks.forEach(s => genMap(s))
  268. }
  269. // parse CSS vars
  270. descriptor.cssVars = parseCssVars(descriptor)
  271. // check if the SFC uses :slotted
  272. const slottedRE = /(?:::v-|:)slotted\(/
  273. descriptor.slotted = descriptor.styles.some(
  274. s => s.scoped && slottedRE.test(s.content),
  275. )
  276. const result = {
  277. descriptor,
  278. errors,
  279. }
  280. parseCache.set(sourceKey, result)
  281. return result
  282. }
  283. function createDuplicateBlockError(
  284. node: ElementNode,
  285. isScriptSetup = false,
  286. ): CompilerError {
  287. const err = new SyntaxError(
  288. `Single file component can contain only one <${node.tag}${
  289. isScriptSetup ? ` setup` : ``
  290. }> element`,
  291. ) as CompilerError
  292. err.loc = node.loc
  293. return err
  294. }
  295. function createBlock(
  296. node: ElementNode,
  297. source: string,
  298. pad: SFCParseOptions['pad'],
  299. ): SFCBlock {
  300. const type = node.tag
  301. const loc = node.innerLoc!
  302. const attrs: Record<string, string | true> = {}
  303. const block: SFCBlock = {
  304. type,
  305. content: source.slice(loc.start.offset, loc.end.offset),
  306. loc,
  307. attrs,
  308. }
  309. if (pad) {
  310. block.content = padContent(source, block, pad) + block.content
  311. }
  312. node.props.forEach(p => {
  313. if (p.type === NodeTypes.ATTRIBUTE) {
  314. const name = p.name
  315. attrs[name] = p.value ? p.value.content || true : true
  316. if (name === 'lang') {
  317. block.lang = p.value && p.value.content
  318. } else if (name === 'src') {
  319. block.src = p.value && p.value.content
  320. } else if (type === 'style') {
  321. if (name === 'scoped') {
  322. ;(block as SFCStyleBlock).scoped = true
  323. } else if (name === 'module') {
  324. ;(block as SFCStyleBlock).module = attrs[name]
  325. }
  326. } else if (type === 'script' && name === 'setup') {
  327. ;(block as SFCScriptBlock).setup = attrs.setup
  328. }
  329. }
  330. })
  331. return block
  332. }
  333. const splitRE = /\r?\n/g
  334. const emptyRE = /^(?:\/\/)?\s*$/
  335. const replaceRE = /./g
  336. function generateSourceMap(
  337. filename: string,
  338. source: string,
  339. generated: string,
  340. sourceRoot: string,
  341. lineOffset: number,
  342. columnOffset: number,
  343. ): RawSourceMap {
  344. const map = new SourceMapGenerator({
  345. file: filename.replace(/\\/g, '/'),
  346. sourceRoot: sourceRoot.replace(/\\/g, '/'),
  347. }) as unknown as CodegenSourceMapGenerator
  348. map.setSourceContent(filename, source)
  349. map._sources.add(filename)
  350. generated.split(splitRE).forEach((line, index) => {
  351. if (!emptyRE.test(line)) {
  352. const originalLine = index + 1 + lineOffset
  353. const generatedLine = index + 1
  354. for (let i = 0; i < line.length; i++) {
  355. if (!/\s/.test(line[i])) {
  356. map._mappings.add({
  357. originalLine,
  358. originalColumn: i + columnOffset,
  359. generatedLine,
  360. generatedColumn: i,
  361. source: filename,
  362. name: null,
  363. })
  364. }
  365. }
  366. }
  367. })
  368. return map.toJSON()
  369. }
  370. function padContent(
  371. content: string,
  372. block: SFCBlock,
  373. pad: SFCParseOptions['pad'],
  374. ): string {
  375. content = content.slice(0, block.loc.start.offset)
  376. if (pad === 'space') {
  377. return content.replace(replaceRE, ' ')
  378. } else {
  379. const offset = content.split(splitRE).length
  380. const padChar = block.type === 'script' && !block.lang ? '//\n' : '\n'
  381. return Array(offset).join(padChar)
  382. }
  383. }
  384. function hasSrc(node: ElementNode) {
  385. return node.props.some(p => {
  386. if (p.type !== NodeTypes.ATTRIBUTE) {
  387. return false
  388. }
  389. return p.name === 'src'
  390. })
  391. }
  392. /**
  393. * Returns true if the node has no children
  394. * once the empty text nodes (trimmed content) have been filtered out.
  395. */
  396. function isEmpty(node: ElementNode) {
  397. for (let i = 0; i < node.children.length; i++) {
  398. const child = node.children[i]
  399. if (child.type !== NodeTypes.TEXT || child.content.trim() !== '') {
  400. return false
  401. }
  402. }
  403. return true
  404. }
  405. /**
  406. * Note: this comparison assumes the prev/next script are already identical,
  407. * and only checks the special case where <script setup lang="ts"> unused import
  408. * pruning result changes due to template changes.
  409. */
  410. export function hmrShouldReload(
  411. prevImports: Record<string, ImportBinding>,
  412. next: SFCDescriptor,
  413. ): boolean {
  414. if (
  415. !next.scriptSetup ||
  416. (next.scriptSetup.lang !== 'ts' && next.scriptSetup.lang !== 'tsx')
  417. ) {
  418. return false
  419. }
  420. // for each previous import, check if its used status remain the same based on
  421. // the next descriptor's template
  422. for (const key in prevImports) {
  423. // if an import was previous unused, but now is used, we need to force
  424. // reload so that the script now includes that import.
  425. if (!prevImports[key].isUsedInTemplate && isImportUsed(key, next)) {
  426. return true
  427. }
  428. }
  429. return false
  430. }
  431. /**
  432. * Dedent a string.
  433. *
  434. * This removes any whitespace that is common to all lines in the string from
  435. * each line in the string.
  436. */
  437. function dedent(s: string): [string, number] {
  438. const lines = s.split('\n')
  439. const minIndent = lines.reduce(function (minIndent, line) {
  440. if (line.trim() === '') {
  441. return minIndent
  442. }
  443. const indent = line.match(/^\s*/)?.[0]?.length || 0
  444. return Math.min(indent, minIndent)
  445. }, Infinity)
  446. if (minIndent === 0) {
  447. return [s, minIndent]
  448. }
  449. return [
  450. lines
  451. .map(function (line) {
  452. return line.slice(minIndent)
  453. })
  454. .join('\n'),
  455. minIndent,
  456. ]
  457. }