2019-11-07 10:58:15 +08:00
|
|
|
import {
|
|
|
|
NodeTypes,
|
|
|
|
ElementNode,
|
2019-12-23 08:44:21 +08:00
|
|
|
SourceLocation,
|
2019-12-23 10:09:39 +08:00
|
|
|
CompilerError,
|
2020-07-11 10:12:25 +08:00
|
|
|
TextModes,
|
|
|
|
BindingMetadata
|
2019-11-07 10:58:15 +08:00
|
|
|
} from '@vue/compiler-core'
|
2020-07-10 06:18:46 +08:00
|
|
|
import * as CompilerDOM from '@vue/compiler-dom'
|
2019-11-29 04:21:02 +08:00
|
|
|
import { RawSourceMap, SourceMapGenerator } from 'source-map'
|
2019-12-23 08:44:21 +08:00
|
|
|
import { TemplateCompiler } from './compileTemplate'
|
2020-08-29 04:21:03 +08:00
|
|
|
import { Statement } from '@babel/types'
|
2020-11-19 00:07:10 +08:00
|
|
|
import { parseCssVars } from './cssVars'
|
2019-11-07 10:58:15 +08:00
|
|
|
|
2020-07-16 04:27:21 +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
|
2019-12-02 23:43:30 +08:00
|
|
|
pad?: boolean | 'line' | 'space'
|
2019-12-23 08:44:21 +08:00
|
|
|
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'
|
2020-07-11 05:10:48 +08:00
|
|
|
setup?: string | boolean
|
2020-07-10 06:18:46 +08:00
|
|
|
bindings?: BindingMetadata
|
2020-08-29 04:21:03 +08:00
|
|
|
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[]
|
2020-11-19 00:07:10 +08:00
|
|
|
cssVars: string[]
|
2019-11-07 10:58:15 +08:00
|
|
|
}
|
|
|
|
|
2019-12-23 08:44:21 +08:00
|
|
|
export interface SFCParseResult {
|
|
|
|
descriptor: SFCDescriptor
|
2020-07-16 04:00:53 +08:00
|
|
|
errors: (CompilerError | SyntaxError)[]
|
2019-12-23 08:44:21 +08:00
|
|
|
}
|
|
|
|
|
2019-11-19 02:29:04 +08:00
|
|
|
const SFC_CACHE_MAX_SIZE = 500
|
2020-04-26 13:24:25 +08:00
|
|
|
const sourceToSFC =
|
|
|
|
__GLOBAL__ || __ESM_BROWSER__
|
|
|
|
? new Map<string, SFCParseResult>()
|
|
|
|
: (new (require('lru-cache'))(SFC_CACHE_MAX_SIZE) as Map<
|
|
|
|
string,
|
|
|
|
SFCParseResult
|
|
|
|
>)
|
2019-12-23 08:44:21 +08:00
|
|
|
|
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',
|
2019-11-29 04:21:02 +08:00
|
|
|
sourceRoot = '',
|
2019-12-23 08:44:21 +08:00
|
|
|
pad = false,
|
2020-07-16 04:27:21 +08:00
|
|
|
compiler = CompilerDOM
|
2019-11-07 10:58:15 +08:00
|
|
|
}: SFCParseOptions = {}
|
2019-12-23 08:44:21 +08:00
|
|
|
): SFCParseResult {
|
|
|
|
const sourceKey =
|
|
|
|
source + sourceMap + filename + sourceRoot + pad + compiler.parse
|
2019-11-19 02:29:04 +08:00
|
|
|
const cache = sourceToSFC.get(sourceKey)
|
|
|
|
if (cache) {
|
|
|
|
return cache
|
|
|
|
}
|
2019-11-07 10:58:15 +08:00
|
|
|
|
2019-12-23 08:44:21 +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: [],
|
2020-11-19 00:07:10 +08:00
|
|
|
customBlocks: [],
|
|
|
|
cssVars: []
|
2019-11-07 10:58:15 +08:00
|
|
|
}
|
2019-11-29 04:22:30 +08:00
|
|
|
|
2020-07-16 04:00:53 +08:00
|
|
|
const errors: (CompilerError | SyntaxError)[] = []
|
2019-12-23 08:44:21 +08:00
|
|
|
const ast = compiler.parse(source, {
|
|
|
|
// there are no components at SFC parsing level
|
2019-11-07 10:58:15 +08:00
|
|
|
isNativeTag: () => true,
|
2019-12-23 08:44:21 +08:00
|
|
|
// preserve all whitespaces
|
|
|
|
isPreTag: () => true,
|
2020-05-07 23:08:17 +08:00
|
|
|
getTextMode: ({ tag, props }, parent) => {
|
2019-12-23 10:09:39 +08:00
|
|
|
// all top level elements except <template> are parsed as raw text
|
|
|
|
// containers
|
2020-05-07 23:08:17 +08:00
|
|
|
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'
|
|
|
|
)
|
|
|
|
) {
|
2019-12-23 10:09:39 +08:00
|
|
|
return TextModes.RAWTEXT
|
|
|
|
} else {
|
|
|
|
return TextModes.DATA
|
|
|
|
}
|
|
|
|
},
|
2019-12-23 08:44:21 +08:00
|
|
|
onError: e => {
|
|
|
|
errors.push(e)
|
|
|
|
}
|
2019-11-07 10:58:15 +08:00
|
|
|
})
|
|
|
|
|
|
|
|
ast.children.forEach(node => {
|
|
|
|
if (node.type !== NodeTypes.ELEMENT) {
|
|
|
|
return
|
|
|
|
}
|
2020-02-04 23:03:32 +08:00
|
|
|
if (!node.children.length && !hasSrc(node)) {
|
2019-11-19 22:10:59 +08:00
|
|
|
return
|
|
|
|
}
|
2019-11-07 10:58:15 +08:00
|
|
|
switch (node.tag) {
|
|
|
|
case 'template':
|
2019-12-23 08:44:21 +08:00
|
|
|
if (!descriptor.template) {
|
|
|
|
descriptor.template = createBlock(
|
|
|
|
node,
|
|
|
|
source,
|
2020-04-04 09:11:26 +08:00
|
|
|
false
|
2019-12-23 08:44:21 +08:00
|
|
|
) as SFCTemplateBlock
|
2019-11-07 10:58:15 +08:00
|
|
|
} else {
|
2020-07-16 04:00:53 +08:00
|
|
|
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
|
2020-07-10 06:18:46 +08:00
|
|
|
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
|
|
|
}
|
2020-07-10 06:18:46 +08:00
|
|
|
if (!isSetup && !descriptor.script) {
|
2020-07-04 03:08:41 +08:00
|
|
|
descriptor.script = block
|
|
|
|
break
|
|
|
|
}
|
2020-07-16 04:00:53 +08:00
|
|
|
errors.push(createDuplicateBlockError(node, isSetup))
|
2019-11-07 10:58:15 +08:00
|
|
|
break
|
|
|
|
case 'style':
|
2019-12-23 08:44:21 +08:00
|
|
|
descriptor.styles.push(createBlock(node, source, pad) as SFCStyleBlock)
|
2019-11-07 10:58:15 +08:00
|
|
|
break
|
|
|
|
default:
|
2019-12-23 08:44:21 +08:00
|
|
|
descriptor.customBlocks.push(createBlock(node, source, pad))
|
2019-11-07 10:58:15 +08:00
|
|
|
break
|
|
|
|
}
|
|
|
|
})
|
|
|
|
|
2020-07-16 05:43:54 +08:00
|
|
|
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
|
2020-07-16 05:43:54 +08:00
|
|
|
}
|
|
|
|
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
|
2020-07-16 05:43:54 +08:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2019-12-11 06:41:56 +08:00
|
|
|
if (sourceMap) {
|
2019-12-14 02:22:30 +08:00
|
|
|
const genMap = (block: SFCBlock | null) => {
|
|
|
|
if (block && !block.src) {
|
|
|
|
block.map = generateSourceMap(
|
|
|
|
filename,
|
|
|
|
source,
|
|
|
|
block.content,
|
|
|
|
sourceRoot,
|
2020-04-04 09:10:22 +08:00
|
|
|
!pad || block.type === 'template' ? block.loc.start.line - 1 : 0
|
2019-12-14 02:22:30 +08:00
|
|
|
)
|
|
|
|
}
|
2019-11-29 04:21:02 +08:00
|
|
|
}
|
2019-12-23 08:44:21 +08:00
|
|
|
genMap(descriptor.template)
|
|
|
|
genMap(descriptor.script)
|
|
|
|
descriptor.styles.forEach(genMap)
|
2020-08-15 05:47:28 +08:00
|
|
|
descriptor.customBlocks.forEach(genMap)
|
2019-11-07 10:58:15 +08:00
|
|
|
}
|
|
|
|
|
2020-11-19 00:07:10 +08:00
|
|
|
// parse CSS vars
|
|
|
|
descriptor.cssVars = parseCssVars(descriptor)
|
|
|
|
|
2019-12-23 08:44:21 +08:00
|
|
|
const result = {
|
|
|
|
descriptor,
|
|
|
|
errors
|
|
|
|
}
|
|
|
|
sourceToSFC.set(sourceKey, result)
|
|
|
|
return result
|
2019-11-07 10:58:15 +08:00
|
|
|
}
|
|
|
|
|
2020-07-16 04:00:53 +08:00
|
|
|
function createDuplicateBlockError(
|
2020-07-04 03:08:41 +08:00
|
|
|
node: ElementNode,
|
|
|
|
isScriptSetup = false
|
2020-07-16 04:00:53 +08:00
|
|
|
): CompilerError {
|
|
|
|
const err = new SyntaxError(
|
2020-07-04 03:08:41 +08:00
|
|
|
`Single file component can contain only one <${node.tag}${
|
|
|
|
isScriptSetup ? ` setup` : ``
|
2020-07-16 04:00:53 +08:00
|
|
|
}> element`
|
|
|
|
) as CompilerError
|
|
|
|
err.loc = node.loc
|
|
|
|
return err
|
2019-11-15 00:50:13 +08:00
|
|
|
}
|
|
|
|
|
2019-12-02 23:43:30 +08:00
|
|
|
function createBlock(
|
|
|
|
node: ElementNode,
|
|
|
|
source: string,
|
|
|
|
pad: SFCParseOptions['pad']
|
|
|
|
): SFCBlock {
|
2019-11-07 10:58:15 +08:00
|
|
|
const type = node.tag
|
2020-02-04 23:03:32 +08:00
|
|
|
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)
|
|
|
|
}
|
2019-12-23 08:44:21 +08:00
|
|
|
const loc = {
|
|
|
|
source: content,
|
|
|
|
start,
|
|
|
|
end
|
|
|
|
}
|
2019-11-07 10:58:15 +08:00
|
|
|
const attrs: Record<string, string | true> = {}
|
|
|
|
const block: SFCBlock = {
|
|
|
|
type,
|
2019-12-23 08:44:21 +08:00
|
|
|
content,
|
|
|
|
loc,
|
2019-11-07 10:58:15 +08:00
|
|
|
attrs
|
|
|
|
}
|
2020-04-04 09:11:26 +08:00
|
|
|
if (pad) {
|
2019-12-02 23:43:30 +08:00
|
|
|
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
|
2020-07-11 05:10:48 +08:00
|
|
|
} else if (type === 'script' && p.name === 'setup') {
|
|
|
|
;(block as SFCScriptBlock).setup = attrs.setup
|
2019-11-07 10:58:15 +08:00
|
|
|
}
|
|
|
|
}
|
|
|
|
})
|
|
|
|
return block
|
|
|
|
}
|
2019-11-29 04:21:02 +08:00
|
|
|
|
|
|
|
const splitRE = /\r?\n/g
|
|
|
|
const emptyRE = /^(?:\/\/)?\s*$/
|
2019-12-02 23:43:30 +08:00
|
|
|
const replaceRE = /./g
|
2019-11-29 04:21:02 +08:00
|
|
|
|
|
|
|
function generateSourceMap(
|
|
|
|
filename: string,
|
|
|
|
source: string,
|
|
|
|
generated: string,
|
|
|
|
sourceRoot: string,
|
2019-12-14 02:22:30 +08:00
|
|
|
lineOffset: number
|
2019-11-29 04:21:02 +08:00
|
|
|
): 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)) {
|
2020-05-07 11:46:33 +08:00
|
|
|
const originalLine = index + 1 + lineOffset
|
|
|
|
const generatedLine = index + 1
|
|
|
|
for (let i = 0; i < line.length; i++) {
|
2020-05-07 21:38:49 +08:00
|
|
|
if (!/\s/.test(line[i])) {
|
|
|
|
map.addMapping({
|
|
|
|
source: filename,
|
|
|
|
original: {
|
|
|
|
line: originalLine,
|
|
|
|
column: i
|
|
|
|
},
|
|
|
|
generated: {
|
|
|
|
line: generatedLine,
|
|
|
|
column: i
|
|
|
|
}
|
|
|
|
})
|
|
|
|
}
|
2020-05-07 11:46:33 +08:00
|
|
|
}
|
2019-11-29 04:21:02 +08:00
|
|
|
}
|
|
|
|
})
|
|
|
|
return JSON.parse(map.toString())
|
|
|
|
}
|
2019-12-02 23:43:30 +08:00
|
|
|
|
|
|
|
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)
|
|
|
|
}
|
|
|
|
}
|
2020-02-04 23:03:32 +08:00
|
|
|
|
|
|
|
function hasSrc(node: ElementNode) {
|
|
|
|
return node.props.some(p => {
|
|
|
|
if (p.type !== NodeTypes.ATTRIBUTE) {
|
|
|
|
return false
|
|
|
|
}
|
|
|
|
return p.name === 'src'
|
|
|
|
})
|
|
|
|
}
|