parse.ts 6.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290
  1. import {
  2. NodeTypes,
  3. ElementNode,
  4. SourceLocation,
  5. CompilerError,
  6. TextModes
  7. } from '@vue/compiler-core'
  8. import { RawSourceMap, SourceMapGenerator } from 'source-map'
  9. import LRUCache from 'lru-cache'
  10. import { generateCodeFrame } from '@vue/shared'
  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. }
  35. export interface SFCStyleBlock extends SFCBlock {
  36. type: 'style'
  37. scoped?: boolean
  38. module?: string | boolean
  39. }
  40. export interface SFCDescriptor {
  41. filename: string
  42. template: SFCTemplateBlock | null
  43. script: SFCScriptBlock | null
  44. styles: SFCStyleBlock[]
  45. customBlocks: SFCBlock[]
  46. }
  47. export interface SFCParseResult {
  48. descriptor: SFCDescriptor
  49. errors: CompilerError[]
  50. }
  51. const SFC_CACHE_MAX_SIZE = 500
  52. const sourceToSFC = new LRUCache<string, SFCParseResult>(SFC_CACHE_MAX_SIZE)
  53. export function parse(
  54. source: string,
  55. {
  56. sourceMap = true,
  57. filename = 'component.vue',
  58. sourceRoot = '',
  59. pad = false,
  60. compiler = require('@vue/compiler-dom')
  61. }: SFCParseOptions = {}
  62. ): SFCParseResult {
  63. const sourceKey =
  64. source + sourceMap + filename + sourceRoot + pad + compiler.parse
  65. const cache = sourceToSFC.get(sourceKey)
  66. if (cache) {
  67. return cache
  68. }
  69. const descriptor: SFCDescriptor = {
  70. filename,
  71. template: null,
  72. script: null,
  73. styles: [],
  74. customBlocks: []
  75. }
  76. const errors: CompilerError[] = []
  77. const ast = compiler.parse(source, {
  78. // there are no components at SFC parsing level
  79. isNativeTag: () => true,
  80. // preserve all whitespaces
  81. isPreTag: () => true,
  82. getTextMode: (tag, _ns, parent) => {
  83. // all top level elements except <template> are parsed as raw text
  84. // containers
  85. if (!parent && tag !== 'template') {
  86. return TextModes.RAWTEXT
  87. } else {
  88. return TextModes.DATA
  89. }
  90. },
  91. onError: e => {
  92. errors.push(e)
  93. }
  94. })
  95. ast.children.forEach(node => {
  96. if (node.type !== NodeTypes.ELEMENT) {
  97. return
  98. }
  99. if (!node.children.length && !hasSrc(node)) {
  100. return
  101. }
  102. switch (node.tag) {
  103. case 'template':
  104. if (!descriptor.template) {
  105. descriptor.template = createBlock(
  106. node,
  107. source,
  108. false
  109. ) as SFCTemplateBlock
  110. } else {
  111. warnDuplicateBlock(source, filename, node)
  112. }
  113. break
  114. case 'script':
  115. if (!descriptor.script) {
  116. descriptor.script = createBlock(node, source, pad) as SFCScriptBlock
  117. } else {
  118. warnDuplicateBlock(source, filename, node)
  119. }
  120. break
  121. case 'style':
  122. descriptor.styles.push(createBlock(node, source, pad) as SFCStyleBlock)
  123. break
  124. default:
  125. descriptor.customBlocks.push(createBlock(node, source, pad))
  126. break
  127. }
  128. })
  129. if (sourceMap) {
  130. const genMap = (block: SFCBlock | null) => {
  131. if (block && !block.src) {
  132. block.map = generateSourceMap(
  133. filename,
  134. source,
  135. block.content,
  136. sourceRoot,
  137. !pad || block.type === 'template' ? block.loc.start.line - 1 : 0
  138. )
  139. }
  140. }
  141. genMap(descriptor.template)
  142. genMap(descriptor.script)
  143. descriptor.styles.forEach(genMap)
  144. }
  145. const result = {
  146. descriptor,
  147. errors
  148. }
  149. sourceToSFC.set(sourceKey, result)
  150. return result
  151. }
  152. function warnDuplicateBlock(
  153. source: string,
  154. filename: string,
  155. node: ElementNode
  156. ) {
  157. const codeFrame = generateCodeFrame(
  158. source,
  159. node.loc.start.offset,
  160. node.loc.end.offset
  161. )
  162. const location = `${filename}:${node.loc.start.line}:${node.loc.start.column}`
  163. console.warn(
  164. `Single file component can contain only one ${
  165. node.tag
  166. } element (${location}):\n\n${codeFrame}`
  167. )
  168. }
  169. function createBlock(
  170. node: ElementNode,
  171. source: string,
  172. pad: SFCParseOptions['pad']
  173. ): SFCBlock {
  174. const type = node.tag
  175. let { start, end } = node.loc
  176. let content = ''
  177. if (node.children.length) {
  178. start = node.children[0].loc.start
  179. end = node.children[node.children.length - 1].loc.end
  180. content = source.slice(start.offset, end.offset)
  181. }
  182. const loc = {
  183. source: content,
  184. start,
  185. end
  186. }
  187. const attrs: Record<string, string | true> = {}
  188. const block: SFCBlock = {
  189. type,
  190. content,
  191. loc,
  192. attrs
  193. }
  194. if (pad) {
  195. block.content = padContent(source, block, pad) + block.content
  196. }
  197. node.props.forEach(p => {
  198. if (p.type === NodeTypes.ATTRIBUTE) {
  199. attrs[p.name] = p.value ? p.value.content || true : true
  200. if (p.name === 'lang') {
  201. block.lang = p.value && p.value.content
  202. } else if (p.name === 'src') {
  203. block.src = p.value && p.value.content
  204. } else if (type === 'style') {
  205. if (p.name === 'scoped') {
  206. ;(block as SFCStyleBlock).scoped = true
  207. } else if (p.name === 'module') {
  208. ;(block as SFCStyleBlock).module = attrs[p.name]
  209. }
  210. } else if (type === 'template' && p.name === 'functional') {
  211. ;(block as SFCTemplateBlock).functional = true
  212. }
  213. }
  214. })
  215. return block
  216. }
  217. const splitRE = /\r?\n/g
  218. const emptyRE = /^(?:\/\/)?\s*$/
  219. const replaceRE = /./g
  220. function generateSourceMap(
  221. filename: string,
  222. source: string,
  223. generated: string,
  224. sourceRoot: string,
  225. lineOffset: number
  226. ): RawSourceMap {
  227. const map = new SourceMapGenerator({
  228. file: filename.replace(/\\/g, '/'),
  229. sourceRoot: sourceRoot.replace(/\\/g, '/')
  230. })
  231. map.setSourceContent(filename, source)
  232. generated.split(splitRE).forEach((line, index) => {
  233. if (!emptyRE.test(line)) {
  234. map.addMapping({
  235. source: filename,
  236. original: {
  237. line: index + 1 + lineOffset,
  238. column: 0
  239. },
  240. generated: {
  241. line: index + 1,
  242. column: 0
  243. }
  244. })
  245. }
  246. })
  247. return JSON.parse(map.toString())
  248. }
  249. function padContent(
  250. content: string,
  251. block: SFCBlock,
  252. pad: SFCParseOptions['pad']
  253. ): string {
  254. content = content.slice(0, block.loc.start.offset)
  255. if (pad === 'space') {
  256. return content.replace(replaceRE, ' ')
  257. } else {
  258. const offset = content.split(splitRE).length
  259. const padChar = block.type === 'script' && !block.lang ? '//\n' : '\n'
  260. return Array(offset).join(padChar)
  261. }
  262. }
  263. function hasSrc(node: ElementNode) {
  264. return node.props.some(p => {
  265. if (p.type !== NodeTypes.ATTRIBUTE) {
  266. return false
  267. }
  268. return p.name === 'src'
  269. })
  270. }