| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345 |
- import {
- NodeTypes,
- ElementNode,
- SourceLocation,
- CompilerError,
- TextModes,
- BindingMetadata
- } from '@vue/compiler-core'
- import * as CompilerDOM from '@vue/compiler-dom'
- import { RawSourceMap, SourceMapGenerator } from 'source-map'
- import { TemplateCompiler } from './compileTemplate'
- export interface SFCParseOptions {
- filename?: string
- sourceMap?: boolean
- sourceRoot?: string
- pad?: boolean | 'line' | 'space'
- compiler?: TemplateCompiler
- }
- export interface SFCBlock {
- type: string
- content: string
- attrs: Record<string, string | true>
- loc: SourceLocation
- map?: RawSourceMap
- lang?: string
- src?: string
- }
- export interface SFCTemplateBlock extends SFCBlock {
- type: 'template'
- functional?: boolean
- }
- export interface SFCScriptBlock extends SFCBlock {
- type: 'script'
- setup?: string | boolean
- bindings?: BindingMetadata
- }
- export interface SFCStyleBlock extends SFCBlock {
- type: 'style'
- scoped?: boolean
- vars?: string
- module?: string | boolean
- }
- export interface SFCDescriptor {
- filename: string
- source: string
- template: SFCTemplateBlock | null
- script: SFCScriptBlock | null
- scriptSetup: SFCScriptBlock | null
- styles: SFCStyleBlock[]
- customBlocks: SFCBlock[]
- }
- export interface SFCParseResult {
- descriptor: SFCDescriptor
- errors: (CompilerError | SyntaxError)[]
- }
- const SFC_CACHE_MAX_SIZE = 500
- const sourceToSFC =
- __GLOBAL__ || __ESM_BROWSER__
- ? new Map<string, SFCParseResult>()
- : (new (require('lru-cache'))(SFC_CACHE_MAX_SIZE) as Map<
- string,
- SFCParseResult
- >)
- export function parse(
- source: string,
- {
- sourceMap = true,
- filename = 'component.vue',
- sourceRoot = '',
- pad = false,
- compiler = CompilerDOM
- }: SFCParseOptions = {}
- ): SFCParseResult {
- const sourceKey =
- source + sourceMap + filename + sourceRoot + pad + compiler.parse
- const cache = sourceToSFC.get(sourceKey)
- if (cache) {
- return cache
- }
- const descriptor: SFCDescriptor = {
- filename,
- source,
- template: null,
- script: null,
- scriptSetup: null,
- styles: [],
- customBlocks: []
- }
- const errors: (CompilerError | SyntaxError)[] = []
- const ast = compiler.parse(source, {
- // there are no components at SFC parsing level
- isNativeTag: () => true,
- // preserve all whitespaces
- isPreTag: () => true,
- getTextMode: ({ tag, props }, parent) => {
- // all top level elements except <template> are parsed as raw text
- // containers
- if (
- (!parent && tag !== 'template') ||
- // <template lang="xxx"> should also be treated as raw text
- props.some(
- p =>
- p.type === NodeTypes.ATTRIBUTE &&
- p.name === 'lang' &&
- p.value &&
- p.value.content !== 'html'
- )
- ) {
- return TextModes.RAWTEXT
- } else {
- return TextModes.DATA
- }
- },
- onError: e => {
- errors.push(e)
- }
- })
- ast.children.forEach(node => {
- if (node.type !== NodeTypes.ELEMENT) {
- return
- }
- if (!node.children.length && !hasSrc(node)) {
- return
- }
- switch (node.tag) {
- case 'template':
- if (!descriptor.template) {
- descriptor.template = createBlock(
- node,
- source,
- false
- ) as SFCTemplateBlock
- } else {
- errors.push(createDuplicateBlockError(node))
- }
- break
- case 'script':
- const block = createBlock(node, source, pad) as SFCScriptBlock
- const isSetup = !!block.attrs.setup
- if (isSetup && !descriptor.scriptSetup) {
- descriptor.scriptSetup = block
- break
- }
- if (!isSetup && !descriptor.script) {
- descriptor.script = block
- break
- }
- errors.push(createDuplicateBlockError(node, isSetup))
- break
- case 'style':
- descriptor.styles.push(createBlock(node, source, pad) as SFCStyleBlock)
- break
- default:
- descriptor.customBlocks.push(createBlock(node, source, pad))
- break
- }
- })
- if (descriptor.scriptSetup) {
- if (descriptor.scriptSetup.src) {
- errors.push(
- new SyntaxError(
- `<script setup> cannot use the "src" attribute because ` +
- `its syntax will be ambiguous outside of the component.`
- )
- )
- delete descriptor.scriptSetup
- }
- if (descriptor.script && descriptor.script.src) {
- errors.push(
- new SyntaxError(
- `<script> cannot use the "src" attribute when <script setup> is ` +
- `also present because they must be processed together.`
- )
- )
- delete descriptor.script
- }
- }
- if (sourceMap) {
- const genMap = (block: SFCBlock | null) => {
- if (block && !block.src) {
- block.map = generateSourceMap(
- filename,
- source,
- block.content,
- sourceRoot,
- !pad || block.type === 'template' ? block.loc.start.line - 1 : 0
- )
- }
- }
- genMap(descriptor.template)
- genMap(descriptor.script)
- descriptor.styles.forEach(genMap)
- }
- const result = {
- descriptor,
- errors
- }
- sourceToSFC.set(sourceKey, result)
- return result
- }
- function createDuplicateBlockError(
- node: ElementNode,
- isScriptSetup = false
- ): CompilerError {
- const err = new SyntaxError(
- `Single file component can contain only one <${node.tag}${
- isScriptSetup ? ` setup` : ``
- }> element`
- ) as CompilerError
- err.loc = node.loc
- return err
- }
- function createBlock(
- node: ElementNode,
- source: string,
- pad: SFCParseOptions['pad']
- ): SFCBlock {
- const type = node.tag
- let { start, end } = node.loc
- let content = ''
- if (node.children.length) {
- start = node.children[0].loc.start
- end = node.children[node.children.length - 1].loc.end
- content = source.slice(start.offset, end.offset)
- }
- const loc = {
- source: content,
- start,
- end
- }
- const attrs: Record<string, string | true> = {}
- const block: SFCBlock = {
- type,
- content,
- loc,
- attrs
- }
- if (pad) {
- block.content = padContent(source, block, pad) + block.content
- }
- node.props.forEach(p => {
- if (p.type === NodeTypes.ATTRIBUTE) {
- attrs[p.name] = p.value ? p.value.content || true : true
- if (p.name === 'lang') {
- block.lang = p.value && p.value.content
- } else if (p.name === 'src') {
- block.src = p.value && p.value.content
- } else if (type === 'style') {
- if (p.name === 'scoped') {
- ;(block as SFCStyleBlock).scoped = true
- } else if (p.name === 'vars' && typeof attrs.vars === 'string') {
- ;(block as SFCStyleBlock).vars = attrs.vars
- } else if (p.name === 'module') {
- ;(block as SFCStyleBlock).module = attrs[p.name]
- }
- } else if (type === 'template' && p.name === 'functional') {
- ;(block as SFCTemplateBlock).functional = true
- } else if (type === 'script' && p.name === 'setup') {
- ;(block as SFCScriptBlock).setup = attrs.setup
- }
- }
- })
- return block
- }
- const splitRE = /\r?\n/g
- const emptyRE = /^(?:\/\/)?\s*$/
- const replaceRE = /./g
- function generateSourceMap(
- filename: string,
- source: string,
- generated: string,
- sourceRoot: string,
- lineOffset: number
- ): RawSourceMap {
- const map = new SourceMapGenerator({
- file: filename.replace(/\\/g, '/'),
- sourceRoot: sourceRoot.replace(/\\/g, '/')
- })
- map.setSourceContent(filename, source)
- generated.split(splitRE).forEach((line, index) => {
- if (!emptyRE.test(line)) {
- const originalLine = index + 1 + lineOffset
- const generatedLine = index + 1
- for (let i = 0; i < line.length; i++) {
- if (!/\s/.test(line[i])) {
- map.addMapping({
- source: filename,
- original: {
- line: originalLine,
- column: i
- },
- generated: {
- line: generatedLine,
- column: i
- }
- })
- }
- }
- }
- })
- return JSON.parse(map.toString())
- }
- function padContent(
- content: string,
- block: SFCBlock,
- pad: SFCParseOptions['pad']
- ): string {
- content = content.slice(0, block.loc.start.offset)
- if (pad === 'space') {
- return content.replace(replaceRE, ' ')
- } else {
- const offset = content.split(splitRE).length
- const padChar = block.type === 'script' && !block.lang ? '//\n' : '\n'
- return Array(offset).join(padChar)
- }
- }
- function hasSrc(node: ElementNode) {
- return node.props.some(p => {
- if (p.type !== NodeTypes.ATTRIBUTE) {
- return false
- }
- return p.name === 'src'
- })
- }
|