| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412 |
- 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'
- import { parseCssVars } from './cssVars'
- import { createCache } from './cache'
- export interface SFCParseOptions {
- filename?: string
- sourceMap?: boolean
- sourceRoot?: string
- pad?: boolean | 'line' | 'space'
- ignoreEmpty?: boolean
- 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'
- ast: ElementNode
- }
- export interface SFCScriptBlock extends SFCBlock {
- type: 'script'
- setup?: string | boolean
- bindings?: BindingMetadata
- /**
- * import('\@babel/types').Statement
- */
- scriptAst?: any[]
- /**
- * import('\@babel/types').Statement
- */
- scriptSetupAst?: any[]
- }
- export interface SFCStyleBlock extends SFCBlock {
- type: 'style'
- scoped?: boolean
- module?: string | boolean
- }
- export interface SFCDescriptor {
- filename: string
- source: string
- template: SFCTemplateBlock | null
- script: SFCScriptBlock | null
- scriptSetup: SFCScriptBlock | null
- styles: SFCStyleBlock[]
- customBlocks: SFCBlock[]
- cssVars: string[]
- // whether the SFC uses :slotted() modifier.
- // this is used as a compiler optimization hint.
- slotted: boolean
- }
- export interface SFCParseResult {
- descriptor: SFCDescriptor
- errors: (CompilerError | SyntaxError)[]
- }
- const sourceToSFC = createCache<SFCParseResult>()
- export function parse(
- source: string,
- {
- sourceMap = true,
- filename = 'anonymous.vue',
- sourceRoot = '',
- pad = false,
- ignoreEmpty = true,
- 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: [],
- cssVars: [],
- slotted: false
- }
- 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
- (tag === 'template' &&
- props.some(
- p =>
- p.type === NodeTypes.ATTRIBUTE &&
- p.name === 'lang' &&
- p.value &&
- p.value.content &&
- 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
- }
- // we only want to keep the nodes that are not empty (when the tag is not a template)
- if (
- ignoreEmpty &&
- node.tag !== 'template' &&
- isEmpty(node) &&
- !hasSrc(node)
- ) {
- return
- }
- switch (node.tag) {
- case 'template':
- if (!descriptor.template) {
- const templateBlock = (descriptor.template = createBlock(
- node,
- source,
- false
- ) as SFCTemplateBlock)
- templateBlock.ast = node
- // warn against 2.x <template functional>
- if (templateBlock.attrs.functional) {
- const err = new SyntaxError(
- `<template functional> is no longer supported in Vue 3, since ` +
- `functional components no longer have significant performance ` +
- `difference from stateful ones. Just use a normal <template> ` +
- `instead.`
- ) as CompilerError
- err.loc = node.props.find(p => p.name === 'functional')!.loc
- errors.push(err)
- }
- } else {
- errors.push(createDuplicateBlockError(node))
- }
- break
- case 'script':
- const scriptBlock = createBlock(node, source, pad) as SFCScriptBlock
- const isSetup = !!scriptBlock.attrs.setup
- if (isSetup && !descriptor.scriptSetup) {
- descriptor.scriptSetup = scriptBlock
- break
- }
- if (!isSetup && !descriptor.script) {
- descriptor.script = scriptBlock
- break
- }
- errors.push(createDuplicateBlockError(node, isSetup))
- break
- case 'style':
- const styleBlock = createBlock(node, source, pad) as SFCStyleBlock
- if (styleBlock.attrs.vars) {
- errors.push(
- new SyntaxError(
- `<style vars> has been replaced by a new proposal: ` +
- `https://github.com/vuejs/rfcs/pull/231`
- )
- )
- }
- descriptor.styles.push(styleBlock)
- 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.`
- )
- )
- descriptor.scriptSetup = null
- }
- 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.`
- )
- )
- descriptor.script = null
- }
- }
- 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)
- descriptor.customBlocks.forEach(genMap)
- }
- // parse CSS vars
- descriptor.cssVars = parseCssVars(descriptor)
- // check if the SFC uses :slotted
- const slottedRE = /(?:::v-|:)slotted\(/
- descriptor.slotted = descriptor.styles.some(
- s => s.scoped && slottedRE.test(s.content)
- )
- 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)
- } else {
- const offset = node.loc.source.indexOf(`</`)
- if (offset > -1) {
- start = {
- line: start.line,
- column: start.column + offset,
- offset: start.offset + offset
- }
- }
- end = { ...start }
- }
- 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 === 'module') {
- ;(block as SFCStyleBlock).module = attrs[p.name]
- }
- } 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'
- })
- }
- /**
- * Returns true if the node has no children
- * once the empty text nodes (trimmed content) have been filtered out.
- */
- function isEmpty(node: ElementNode) {
- return (
- node.children.filter(
- child => child.type !== NodeTypes.TEXT || child.content.trim() !== ''
- ).length === 0
- )
- }
|