parse.ts 6.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277
  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) {
  100. return
  101. }
  102. switch (node.tag) {
  103. case 'template':
  104. if (!descriptor.template) {
  105. descriptor.template = createBlock(
  106. node,
  107. source,
  108. pad
  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 ? 0 : block.loc.start.line - 1
  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. const start = node.children[0].loc.start
  176. const end = node.children[node.children.length - 1].loc.end
  177. const content = source.slice(start.offset, end.offset)
  178. const loc = {
  179. source: content,
  180. start,
  181. end
  182. }
  183. const attrs: Record<string, string | true> = {}
  184. const block: SFCBlock = {
  185. type,
  186. content,
  187. loc,
  188. attrs
  189. }
  190. if (node.tag !== 'template' && pad) {
  191. block.content = padContent(source, block, pad) + block.content
  192. }
  193. node.props.forEach(p => {
  194. if (p.type === NodeTypes.ATTRIBUTE) {
  195. attrs[p.name] = p.value ? p.value.content || true : true
  196. if (p.name === 'lang') {
  197. block.lang = p.value && p.value.content
  198. } else if (p.name === 'src') {
  199. block.src = p.value && p.value.content
  200. } else if (type === 'style') {
  201. if (p.name === 'scoped') {
  202. ;(block as SFCStyleBlock).scoped = true
  203. } else if (p.name === 'module') {
  204. ;(block as SFCStyleBlock).module = attrs[p.name]
  205. }
  206. } else if (type === 'template' && p.name === 'functional') {
  207. ;(block as SFCTemplateBlock).functional = true
  208. }
  209. }
  210. })
  211. return block
  212. }
  213. const splitRE = /\r?\n/g
  214. const emptyRE = /^(?:\/\/)?\s*$/
  215. const replaceRE = /./g
  216. function generateSourceMap(
  217. filename: string,
  218. source: string,
  219. generated: string,
  220. sourceRoot: string,
  221. lineOffset: number
  222. ): RawSourceMap {
  223. const map = new SourceMapGenerator({
  224. file: filename.replace(/\\/g, '/'),
  225. sourceRoot: sourceRoot.replace(/\\/g, '/')
  226. })
  227. map.setSourceContent(filename, source)
  228. generated.split(splitRE).forEach((line, index) => {
  229. if (!emptyRE.test(line)) {
  230. map.addMapping({
  231. source: filename,
  232. original: {
  233. line: index + 1 + lineOffset,
  234. column: 0
  235. },
  236. generated: {
  237. line: index + 1,
  238. column: 0
  239. }
  240. })
  241. }
  242. })
  243. return JSON.parse(map.toString())
  244. }
  245. function padContent(
  246. content: string,
  247. block: SFCBlock,
  248. pad: SFCParseOptions['pad']
  249. ): string {
  250. content = content.slice(0, block.loc.start.offset)
  251. if (pad === 'space') {
  252. return content.replace(replaceRE, ' ')
  253. } else {
  254. const offset = content.split(splitRE).length
  255. const padChar = block.type === 'script' && !block.lang ? '//\n' : '\n'
  256. return Array(offset).join(padChar)
  257. }
  258. }