parse.ts 8.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345
  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. export interface SFCParseOptions {
  13. filename?: string
  14. sourceMap?: boolean
  15. sourceRoot?: string
  16. pad?: boolean | 'line' | 'space'
  17. compiler?: TemplateCompiler
  18. }
  19. export interface SFCBlock {
  20. type: string
  21. content: string
  22. attrs: Record<string, string | true>
  23. loc: SourceLocation
  24. map?: RawSourceMap
  25. lang?: string
  26. src?: string
  27. }
  28. export interface SFCTemplateBlock extends SFCBlock {
  29. type: 'template'
  30. functional?: boolean
  31. }
  32. export interface SFCScriptBlock extends SFCBlock {
  33. type: 'script'
  34. setup?: string | boolean
  35. bindings?: BindingMetadata
  36. }
  37. export interface SFCStyleBlock extends SFCBlock {
  38. type: 'style'
  39. scoped?: boolean
  40. vars?: string
  41. module?: string | boolean
  42. }
  43. export interface SFCDescriptor {
  44. filename: string
  45. source: string
  46. template: SFCTemplateBlock | null
  47. script: SFCScriptBlock | null
  48. scriptSetup: SFCScriptBlock | null
  49. styles: SFCStyleBlock[]
  50. customBlocks: SFCBlock[]
  51. }
  52. export interface SFCParseResult {
  53. descriptor: SFCDescriptor
  54. errors: (CompilerError | SyntaxError)[]
  55. }
  56. const SFC_CACHE_MAX_SIZE = 500
  57. const sourceToSFC =
  58. __GLOBAL__ || __ESM_BROWSER__
  59. ? new Map<string, SFCParseResult>()
  60. : (new (require('lru-cache'))(SFC_CACHE_MAX_SIZE) as Map<
  61. string,
  62. SFCParseResult
  63. >)
  64. export function parse(
  65. source: string,
  66. {
  67. sourceMap = true,
  68. filename = 'component.vue',
  69. sourceRoot = '',
  70. pad = false,
  71. compiler = CompilerDOM
  72. }: SFCParseOptions = {}
  73. ): SFCParseResult {
  74. const sourceKey =
  75. source + sourceMap + filename + sourceRoot + pad + compiler.parse
  76. const cache = sourceToSFC.get(sourceKey)
  77. if (cache) {
  78. return cache
  79. }
  80. const descriptor: SFCDescriptor = {
  81. filename,
  82. source,
  83. template: null,
  84. script: null,
  85. scriptSetup: null,
  86. styles: [],
  87. customBlocks: []
  88. }
  89. const errors: (CompilerError | SyntaxError)[] = []
  90. const ast = compiler.parse(source, {
  91. // there are no components at SFC parsing level
  92. isNativeTag: () => true,
  93. // preserve all whitespaces
  94. isPreTag: () => true,
  95. getTextMode: ({ tag, props }, parent) => {
  96. // all top level elements except <template> are parsed as raw text
  97. // containers
  98. if (
  99. (!parent && tag !== 'template') ||
  100. // <template lang="xxx"> should also be treated as raw text
  101. props.some(
  102. p =>
  103. p.type === NodeTypes.ATTRIBUTE &&
  104. p.name === 'lang' &&
  105. p.value &&
  106. p.value.content !== 'html'
  107. )
  108. ) {
  109. return TextModes.RAWTEXT
  110. } else {
  111. return TextModes.DATA
  112. }
  113. },
  114. onError: e => {
  115. errors.push(e)
  116. }
  117. })
  118. ast.children.forEach(node => {
  119. if (node.type !== NodeTypes.ELEMENT) {
  120. return
  121. }
  122. if (!node.children.length && !hasSrc(node)) {
  123. return
  124. }
  125. switch (node.tag) {
  126. case 'template':
  127. if (!descriptor.template) {
  128. descriptor.template = createBlock(
  129. node,
  130. source,
  131. false
  132. ) as SFCTemplateBlock
  133. } else {
  134. errors.push(createDuplicateBlockError(node))
  135. }
  136. break
  137. case 'script':
  138. const block = createBlock(node, source, pad) as SFCScriptBlock
  139. const isSetup = !!block.attrs.setup
  140. if (isSetup && !descriptor.scriptSetup) {
  141. descriptor.scriptSetup = block
  142. break
  143. }
  144. if (!isSetup && !descriptor.script) {
  145. descriptor.script = block
  146. break
  147. }
  148. errors.push(createDuplicateBlockError(node, isSetup))
  149. break
  150. case 'style':
  151. descriptor.styles.push(createBlock(node, source, pad) as SFCStyleBlock)
  152. break
  153. default:
  154. descriptor.customBlocks.push(createBlock(node, source, pad))
  155. break
  156. }
  157. })
  158. if (descriptor.scriptSetup) {
  159. if (descriptor.scriptSetup.src) {
  160. errors.push(
  161. new SyntaxError(
  162. `<script setup> cannot use the "src" attribute because ` +
  163. `its syntax will be ambiguous outside of the component.`
  164. )
  165. )
  166. delete descriptor.scriptSetup
  167. }
  168. if (descriptor.script && descriptor.script.src) {
  169. errors.push(
  170. new SyntaxError(
  171. `<script> cannot use the "src" attribute when <script setup> is ` +
  172. `also present because they must be processed together.`
  173. )
  174. )
  175. delete descriptor.script
  176. }
  177. }
  178. if (sourceMap) {
  179. const genMap = (block: SFCBlock | null) => {
  180. if (block && !block.src) {
  181. block.map = generateSourceMap(
  182. filename,
  183. source,
  184. block.content,
  185. sourceRoot,
  186. !pad || block.type === 'template' ? block.loc.start.line - 1 : 0
  187. )
  188. }
  189. }
  190. genMap(descriptor.template)
  191. genMap(descriptor.script)
  192. descriptor.styles.forEach(genMap)
  193. }
  194. const result = {
  195. descriptor,
  196. errors
  197. }
  198. sourceToSFC.set(sourceKey, result)
  199. return result
  200. }
  201. function createDuplicateBlockError(
  202. node: ElementNode,
  203. isScriptSetup = false
  204. ): CompilerError {
  205. const err = new SyntaxError(
  206. `Single file component can contain only one <${node.tag}${
  207. isScriptSetup ? ` setup` : ``
  208. }> element`
  209. ) as CompilerError
  210. err.loc = node.loc
  211. return err
  212. }
  213. function createBlock(
  214. node: ElementNode,
  215. source: string,
  216. pad: SFCParseOptions['pad']
  217. ): SFCBlock {
  218. const type = node.tag
  219. let { start, end } = node.loc
  220. let content = ''
  221. if (node.children.length) {
  222. start = node.children[0].loc.start
  223. end = node.children[node.children.length - 1].loc.end
  224. content = source.slice(start.offset, end.offset)
  225. }
  226. const loc = {
  227. source: content,
  228. start,
  229. end
  230. }
  231. const attrs: Record<string, string | true> = {}
  232. const block: SFCBlock = {
  233. type,
  234. content,
  235. loc,
  236. attrs
  237. }
  238. if (pad) {
  239. block.content = padContent(source, block, pad) + block.content
  240. }
  241. node.props.forEach(p => {
  242. if (p.type === NodeTypes.ATTRIBUTE) {
  243. attrs[p.name] = p.value ? p.value.content || true : true
  244. if (p.name === 'lang') {
  245. block.lang = p.value && p.value.content
  246. } else if (p.name === 'src') {
  247. block.src = p.value && p.value.content
  248. } else if (type === 'style') {
  249. if (p.name === 'scoped') {
  250. ;(block as SFCStyleBlock).scoped = true
  251. } else if (p.name === 'vars' && typeof attrs.vars === 'string') {
  252. ;(block as SFCStyleBlock).vars = attrs.vars
  253. } else if (p.name === 'module') {
  254. ;(block as SFCStyleBlock).module = attrs[p.name]
  255. }
  256. } else if (type === 'template' && p.name === 'functional') {
  257. ;(block as SFCTemplateBlock).functional = true
  258. } else if (type === 'script' && p.name === 'setup') {
  259. ;(block as SFCScriptBlock).setup = attrs.setup
  260. }
  261. }
  262. })
  263. return block
  264. }
  265. const splitRE = /\r?\n/g
  266. const emptyRE = /^(?:\/\/)?\s*$/
  267. const replaceRE = /./g
  268. function generateSourceMap(
  269. filename: string,
  270. source: string,
  271. generated: string,
  272. sourceRoot: string,
  273. lineOffset: number
  274. ): RawSourceMap {
  275. const map = new SourceMapGenerator({
  276. file: filename.replace(/\\/g, '/'),
  277. sourceRoot: sourceRoot.replace(/\\/g, '/')
  278. })
  279. map.setSourceContent(filename, source)
  280. generated.split(splitRE).forEach((line, index) => {
  281. if (!emptyRE.test(line)) {
  282. const originalLine = index + 1 + lineOffset
  283. const generatedLine = index + 1
  284. for (let i = 0; i < line.length; i++) {
  285. if (!/\s/.test(line[i])) {
  286. map.addMapping({
  287. source: filename,
  288. original: {
  289. line: originalLine,
  290. column: i
  291. },
  292. generated: {
  293. line: generatedLine,
  294. column: i
  295. }
  296. })
  297. }
  298. }
  299. }
  300. })
  301. return JSON.parse(map.toString())
  302. }
  303. function padContent(
  304. content: string,
  305. block: SFCBlock,
  306. pad: SFCParseOptions['pad']
  307. ): string {
  308. content = content.slice(0, block.loc.start.offset)
  309. if (pad === 'space') {
  310. return content.replace(replaceRE, ' ')
  311. } else {
  312. const offset = content.split(splitRE).length
  313. const padChar = block.type === 'script' && !block.lang ? '//\n' : '\n'
  314. return Array(offset).join(padChar)
  315. }
  316. }
  317. function hasSrc(node: ElementNode) {
  318. return node.props.some(p => {
  319. if (p.type !== NodeTypes.ATTRIBUTE) {
  320. return false
  321. }
  322. return p.name === 'src'
  323. })
  324. }