vue3-yuanma/packages/compiler-sfc/src/parse.ts

353 lines
8.6 KiB
TypeScript
Raw Normal View History

2019-11-07 10:58:15 +08:00
import {
NodeTypes,
ElementNode,
SourceLocation,
CompilerError,
2020-07-11 10:12:25 +08:00
TextModes,
BindingMetadata
2019-11-07 10:58:15 +08:00
} from '@vue/compiler-core'
import * as CompilerDOM from '@vue/compiler-dom'
import { RawSourceMap, SourceMapGenerator } from 'source-map'
import { TemplateCompiler } from './compileTemplate'
import { Statement } from '@babel/types'
import { parseCssVars } from './cssVars'
2019-11-07 10:58:15 +08:00
export interface SFCParseOptions {
2019-11-07 10:58:15 +08:00
filename?: string
2019-12-11 06:41:56 +08:00
sourceMap?: boolean
2019-11-07 10:58:15 +08:00
sourceRoot?: string
pad?: boolean | 'line' | 'space'
compiler?: TemplateCompiler
2019-11-07 10:58:15 +08:00
}
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
scriptAst?: Statement[]
scriptSetupAst?: Statement[]
2019-11-07 10:58:15 +08:00
}
export interface SFCStyleBlock extends SFCBlock {
type: 'style'
scoped?: boolean
module?: string | boolean
}
export interface SFCDescriptor {
filename: string
2020-07-07 03:56:24 +08:00
source: string
2019-11-07 10:58:15 +08:00
template: SFCTemplateBlock | null
script: SFCScriptBlock | null
2020-07-04 03:08:41 +08:00
scriptSetup: SFCScriptBlock | null
2019-11-07 10:58:15 +08:00
styles: SFCStyleBlock[]
customBlocks: SFCBlock[]
cssVars: string[]
2019-11-07 10:58:15 +08:00
}
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
>)
2019-11-07 10:58:15 +08:00
export function parse(
source: string,
{
2019-12-11 06:41:56 +08:00
sourceMap = true,
2019-11-07 10:58:15 +08:00
filename = 'component.vue',
sourceRoot = '',
pad = false,
compiler = CompilerDOM
2019-11-07 10:58:15 +08:00
}: SFCParseOptions = {}
): SFCParseResult {
const sourceKey =
source + sourceMap + filename + sourceRoot + pad + compiler.parse
const cache = sourceToSFC.get(sourceKey)
if (cache) {
return cache
}
2019-11-07 10:58:15 +08:00
const descriptor: SFCDescriptor = {
2019-11-07 10:58:15 +08:00
filename,
2020-07-07 03:56:24 +08:00
source,
2019-11-07 10:58:15 +08:00
template: null,
script: null,
2020-07-04 03:08:41 +08:00
scriptSetup: null,
2019-11-07 10:58:15 +08:00
styles: [],
customBlocks: [],
cssVars: []
2019-11-07 10:58:15 +08:00
}
2019-11-29 04:22:30 +08:00
const errors: (CompilerError | SyntaxError)[] = []
const ast = compiler.parse(source, {
// there are no components at SFC parsing level
2019-11-07 10:58:15 +08:00
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)
}
2019-11-07 10:58:15 +08:00
})
ast.children.forEach(node => {
if (node.type !== NodeTypes.ELEMENT) {
return
}
if (!node.children.length && !hasSrc(node)) {
return
}
2019-11-07 10:58:15 +08:00
switch (node.tag) {
case 'template':
if (!descriptor.template) {
descriptor.template = createBlock(
node,
source,
false
) as SFCTemplateBlock
2019-11-07 10:58:15 +08:00
} else {
errors.push(createDuplicateBlockError(node))
2019-11-07 10:58:15 +08:00
}
break
case 'script':
2020-07-04 03:08:41 +08:00
const block = createBlock(node, source, pad) as SFCScriptBlock
const isSetup = !!block.attrs.setup
if (isSetup && !descriptor.scriptSetup) {
2020-07-04 03:08:41 +08:00
descriptor.scriptSetup = block
break
2019-11-07 10:58:15 +08:00
}
if (!isSetup && !descriptor.script) {
2020-07-04 03:08:41 +08:00
descriptor.script = block
break
}
errors.push(createDuplicateBlockError(node, isSetup))
2019-11-07 10:58:15 +08:00
break
case 'style':
descriptor.styles.push(createBlock(node, source, pad) as SFCStyleBlock)
2019-11-07 10:58:15 +08:00
break
default:
descriptor.customBlocks.push(createBlock(node, source, pad))
2019-11-07 10:58:15 +08:00
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.`
)
)
2020-08-21 05:48:28 +08:00
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.`
)
)
2020-08-21 05:48:28 +08:00
descriptor.script = null
}
}
2019-12-11 06:41:56 +08:00
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)
2019-11-07 10:58:15 +08:00
}
// parse CSS vars
descriptor.cssVars = parseCssVars(descriptor)
const result = {
descriptor,
errors
}
sourceToSFC.set(sourceKey, result)
return result
2019-11-07 10:58:15 +08:00
}
function createDuplicateBlockError(
2020-07-04 03:08:41 +08:00
node: ElementNode,
isScriptSetup = false
): CompilerError {
const err = new SyntaxError(
2020-07-04 03:08:41 +08:00
`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 {
2019-11-07 10:58:15 +08:00
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
}
2019-11-07 10:58:15 +08:00
const attrs: Record<string, string | true> = {}
const block: SFCBlock = {
type,
content,
loc,
2019-11-07 10:58:15 +08:00
attrs
}
if (pad) {
block.content = padContent(source, block, pad) + block.content
}
2019-11-07 10:58:15 +08:00
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 === 'template' && p.name === 'functional') {
;(block as SFCTemplateBlock).functional = true
} else if (type === 'script' && p.name === 'setup') {
;(block as SFCScriptBlock).setup = attrs.setup
2019-11-07 10:58:15 +08:00
}
}
})
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'
})
}