You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
221 lines
5.6 KiB
TypeScript
221 lines
5.6 KiB
TypeScript
import deindent from 'de-indent'
|
|
import { parseHTML } from 'compiler/parser/html-parser'
|
|
import { makeMap } from 'shared/util'
|
|
import { ASTAttr, WarningMessage } from 'types/compiler'
|
|
import { BindingMetadata, RawSourceMap } from './types'
|
|
import type { ImportBinding } from './compileScript'
|
|
|
|
export const DEFAULT_FILENAME = 'anonymous.vue'
|
|
|
|
const splitRE = /\r?\n/g
|
|
const replaceRE = /./g
|
|
const isSpecialTag = makeMap('script,style,template', true)
|
|
|
|
export interface SFCCustomBlock {
|
|
type: string
|
|
content: string
|
|
attrs: { [key: string]: string | true }
|
|
start: number
|
|
end: number
|
|
src?: string
|
|
map?: RawSourceMap
|
|
}
|
|
|
|
export interface SFCBlock extends SFCCustomBlock {
|
|
lang?: string
|
|
scoped?: boolean
|
|
module?: string | boolean
|
|
}
|
|
|
|
export interface SFCScriptBlock extends SFCBlock {
|
|
type: 'script'
|
|
setup?: string | boolean
|
|
bindings?: BindingMetadata
|
|
imports?: Record<string, ImportBinding>
|
|
/**
|
|
* import('\@babel/types').Statement
|
|
*/
|
|
scriptAst?: any[]
|
|
/**
|
|
* import('\@babel/types').Statement
|
|
*/
|
|
scriptSetupAst?: any[]
|
|
}
|
|
|
|
export interface SFCDescriptor {
|
|
source: string
|
|
filename: string
|
|
template: SFCBlock | null
|
|
script: SFCScriptBlock | null
|
|
scriptSetup: SFCScriptBlock | null
|
|
styles: SFCBlock[]
|
|
customBlocks: SFCCustomBlock[]
|
|
cssVars: string[]
|
|
|
|
errors: (string | WarningMessage)[]
|
|
|
|
/**
|
|
* compare with an existing descriptor to determine whether HMR should perform
|
|
* a reload vs. re-render.
|
|
*
|
|
* Note: this comparison assumes the prev/next script are already identical,
|
|
* and only checks the special case where `<script setup lang="ts">` unused
|
|
* import pruning result changes due to template changes.
|
|
*/
|
|
shouldForceReload: (prevImports: Record<string, ImportBinding>) => boolean
|
|
}
|
|
|
|
export interface VueTemplateCompilerParseOptions {
|
|
pad?: 'line' | 'space' | boolean
|
|
deindent?: boolean
|
|
outputSourceRange?: boolean
|
|
}
|
|
|
|
/**
|
|
* Parse a single-file component (*.vue) file into an SFC Descriptor Object.
|
|
*/
|
|
export function parseComponent(
|
|
source: string,
|
|
options: VueTemplateCompilerParseOptions = {}
|
|
): SFCDescriptor {
|
|
const sfc: SFCDescriptor = {
|
|
source,
|
|
filename: DEFAULT_FILENAME,
|
|
template: null,
|
|
script: null,
|
|
scriptSetup: null, // TODO
|
|
styles: [],
|
|
customBlocks: [],
|
|
cssVars: [],
|
|
errors: [],
|
|
shouldForceReload: null as any // attached in parse() by compiler-sfc
|
|
}
|
|
let depth = 0
|
|
let currentBlock: SFCBlock | null = null
|
|
|
|
let warn: any = msg => {
|
|
sfc.errors.push(msg)
|
|
}
|
|
|
|
if (__DEV__ && options.outputSourceRange) {
|
|
warn = (msg, range) => {
|
|
const data: WarningMessage = { msg }
|
|
if (range.start != null) {
|
|
data.start = range.start
|
|
}
|
|
if (range.end != null) {
|
|
data.end = range.end
|
|
}
|
|
sfc.errors.push(data)
|
|
}
|
|
}
|
|
|
|
function start(
|
|
tag: string,
|
|
attrs: ASTAttr[],
|
|
unary: boolean,
|
|
start: number,
|
|
end: number
|
|
) {
|
|
if (depth === 0) {
|
|
currentBlock = {
|
|
type: tag,
|
|
content: '',
|
|
start: end,
|
|
end: 0, // will be set on tag close
|
|
attrs: attrs.reduce((cumulated, { name, value }) => {
|
|
cumulated[name] = value || true
|
|
return cumulated
|
|
}, {})
|
|
}
|
|
|
|
if (typeof currentBlock.attrs.src === 'string') {
|
|
currentBlock.src = currentBlock.attrs.src
|
|
}
|
|
|
|
if (isSpecialTag(tag)) {
|
|
checkAttrs(currentBlock, attrs)
|
|
if (tag === 'script') {
|
|
const block = currentBlock as SFCScriptBlock
|
|
if (block.attrs.setup) {
|
|
block.setup = currentBlock.attrs.setup
|
|
sfc.scriptSetup = block
|
|
} else {
|
|
sfc.script = block
|
|
}
|
|
} else if (tag === 'style') {
|
|
sfc.styles.push(currentBlock)
|
|
} else {
|
|
sfc[tag] = currentBlock
|
|
}
|
|
} else {
|
|
// custom blocks
|
|
sfc.customBlocks.push(currentBlock)
|
|
}
|
|
}
|
|
if (!unary) {
|
|
depth++
|
|
}
|
|
}
|
|
|
|
function checkAttrs(block: SFCBlock, attrs: ASTAttr[]) {
|
|
for (let i = 0; i < attrs.length; i++) {
|
|
const attr = attrs[i]
|
|
if (attr.name === 'lang') {
|
|
block.lang = attr.value
|
|
}
|
|
if (attr.name === 'scoped') {
|
|
block.scoped = true
|
|
}
|
|
if (attr.name === 'module') {
|
|
block.module = attr.value || true
|
|
}
|
|
}
|
|
}
|
|
|
|
function end(tag: string, start: number) {
|
|
if (depth === 1 && currentBlock) {
|
|
currentBlock.end = start
|
|
let text = source.slice(currentBlock.start, currentBlock.end)
|
|
if (
|
|
options.deindent === true ||
|
|
// by default, deindent unless it's script with default lang or (j/t)sx?
|
|
(options.deindent !== false &&
|
|
!(
|
|
currentBlock.type === 'script' &&
|
|
(!currentBlock.lang || /^(j|t)sx?$/.test(currentBlock.lang))
|
|
))
|
|
) {
|
|
text = deindent(text)
|
|
}
|
|
// pad content so that linters and pre-processors can output correct
|
|
// line numbers in errors and warnings
|
|
if (currentBlock.type !== 'template' && options.pad) {
|
|
text = padContent(currentBlock, options.pad) + text
|
|
}
|
|
currentBlock.content = text
|
|
currentBlock = null
|
|
}
|
|
depth--
|
|
}
|
|
|
|
function padContent(block: SFCBlock, pad: true | 'line' | 'space') {
|
|
if (pad === 'space') {
|
|
return source.slice(0, block.start).replace(replaceRE, ' ')
|
|
} else {
|
|
const offset = source.slice(0, block.start).split(splitRE).length
|
|
const padChar = block.type === 'script' && !block.lang ? '//\n' : '\n'
|
|
return Array(offset).join(padChar)
|
|
}
|
|
}
|
|
|
|
parseHTML(source, {
|
|
warn,
|
|
start,
|
|
end,
|
|
outputSourceRange: options.outputSourceRange
|
|
})
|
|
|
|
return sfc
|
|
}
|