refactor: expose parse in compiler-dom, improve sfc parse error handling

This commit is contained in:
Evan You
2019-12-22 19:44:21 -05:00
parent 7d436ab59a
commit 90ddb7c260
33 changed files with 243 additions and 147 deletions

View File

@@ -23,7 +23,7 @@ body
</template>
`,
{ filename: 'example.vue', sourceMap: true }
).template as SFCTemplateBlock
).descriptor.template as SFCTemplateBlock
const result = compileTemplate({
filename: 'example.vue',
@@ -35,10 +35,10 @@ body
})
test('warn missing preprocessor', () => {
const template = parse(`<template lang="unknownLang">\n</template>\n`, {
const template = parse(`<template lang="unknownLang">hi</template>\n`, {
filename: 'example.vue',
sourceMap: true
}).template as SFCTemplateBlock
}).descriptor.template as SFCTemplateBlock
const result = compileTemplate({
filename: 'example.vue',
@@ -70,10 +70,10 @@ test('source map', () => {
`
<template>
<div><p>{{ render }}</p></div>
</template>
</template>
`,
{ filename: 'example.vue', sourceMap: true }
).template as SFCTemplateBlock
).descriptor.template as SFCTemplateBlock
const result = compileTemplate({
filename: 'example.vue',
@@ -86,7 +86,7 @@ test('source map', () => {
test('template errors', () => {
const result = compileTemplate({
filename: 'example.vue',
source: `<div :foo
source: `<div :foo
:bar="a[" v-model="baz"/>`
})
expect(result.errors).toMatchSnapshot()
@@ -100,7 +100,7 @@ test('preprocessor errors', () => {
</template>
`,
{ filename: 'example.vue', sourceMap: true }
).template as SFCTemplateBlock
).descriptor.template as SFCTemplateBlock
const result = compileTemplate({
filename: 'example.vue',

View File

@@ -1,5 +1,6 @@
import { parse } from '../src'
import { mockWarn } from '@vue/runtime-test'
import { baseParse, baseCompile } from '@vue/compiler-core'
describe('compiler:sfc', () => {
mockWarn()
@@ -7,13 +8,14 @@ describe('compiler:sfc', () => {
describe('source map', () => {
test('style block', () => {
const style = parse(`<style>\n.color {\n color: red;\n }\n</style>\n`)
.styles[0]
.descriptor.styles[0]
// TODO need to actually test this with SourceMapConsumer
expect(style.map).not.toBeUndefined()
})
test('script block', () => {
const script = parse(`<script>\nconsole.log(1)\n }\n</script>\n`).script
const script = parse(`<script>\nconsole.log(1)\n }\n</script>\n`)
.descriptor.script
// TODO need to actually test this with SourceMapConsumer
expect(script!.map).not.toBeUndefined()
})
@@ -30,12 +32,12 @@ export default {}
<style>
h1 { color: red }
</style>`
const padFalse = parse(content.trim(), { pad: false })
const padFalse = parse(content.trim(), { pad: false }).descriptor
expect(padFalse.template!.content).toBe('\n<div></div>\n')
expect(padFalse.script!.content).toBe('\nexport default {}\n')
expect(padFalse.styles[0].content).toBe('\nh1 { color: red }\n')
const padTrue = parse(content.trim(), { pad: true })
const padTrue = parse(content.trim(), { pad: true }).descriptor
expect(padTrue.script!.content).toBe(
Array(3 + 1).join('//\n') + '\nexport default {}\n'
)
@@ -43,7 +45,7 @@ h1 { color: red }
Array(6 + 1).join('\n') + '\nh1 { color: red }\n'
)
const padLine = parse(content.trim(), { pad: 'line' })
const padLine = parse(content.trim(), { pad: 'line' }).descriptor
expect(padLine.script!.content).toBe(
Array(3 + 1).join('//\n') + '\nexport default {}\n'
)
@@ -51,7 +53,7 @@ h1 { color: red }
Array(6 + 1).join('\n') + '\nh1 { color: red }\n'
)
const padSpace = parse(content.trim(), { pad: 'space' })
const padSpace = parse(content.trim(), { pad: 'space' }).descriptor
expect(padSpace.script!.content).toBe(
`<template>\n<div></div>\n</template>\n<script>`.replace(/./g, ' ') +
'\nexport default {}\n'
@@ -65,13 +67,42 @@ h1 { color: red }
})
test('should ignore nodes with no content', () => {
expect(parse(`<template/>`).template).toBe(null)
expect(parse(`<script/>`).script).toBe(null)
expect(parse(`<style/>`).styles.length).toBe(0)
expect(parse(`<custom/>`).customBlocks.length).toBe(0)
expect(parse(`<template/>`).descriptor.template).toBe(null)
expect(parse(`<script/>`).descriptor.script).toBe(null)
expect(parse(`<style/>`).descriptor.styles.length).toBe(0)
expect(parse(`<custom/>`).descriptor.customBlocks.length).toBe(0)
})
describe('error', () => {
test('nested templates', () => {
const content = `
<template v-if="ok">ok</template>
<div><div></div></div>
`
const sfc = parse(`<template>${content}</template>`).descriptor
expect(sfc.template!.content).toBe(content)
})
test('error tolerance', () => {
const { errors } = parse(`<template>`)
expect(errors.length).toBe(1)
})
test('should parse as DOM by default', () => {
const { errors } = parse(`<template><input></template>`)
expect(errors.length).toBe(0)
})
test('custom compiler', () => {
const { errors } = parse(`<template><input></template>`, {
compiler: {
parse: baseParse,
compile: baseCompile
}
})
expect(errors.length).toBe(1)
})
describe('warnings', () => {
test('should only allow single template element', () => {
parse(`<template><div/></template><template><div/></template>`)
expect(

View File

@@ -1,10 +1,10 @@
import { generate, parse, transform } from '@vue/compiler-core'
import { generate, baseParse, transform } from '@vue/compiler-core'
import { transformAssetUrl } from '../src/templateTransformAssetUrl'
import { transformElement } from '../../compiler-core/src/transforms/transformElement'
import { transformBind } from '../../compiler-core/src/transforms/vBind'
function compileWithAssetUrls(template: string) {
const ast = parse(template)
const ast = baseParse(template)
transform(ast, {
nodeTransforms: [transformAssetUrl, transformElement],
directiveTransforms: {

View File

@@ -1,10 +1,10 @@
import { generate, parse, transform } from '@vue/compiler-core'
import { generate, baseParse, transform } from '@vue/compiler-core'
import { transformSrcset } from '../src/templateTransformSrcset'
import { transformElement } from '../../compiler-core/src/transforms/transformElement'
import { transformBind } from '../../compiler-core/src/transforms/vBind'
function compileWithSrcset(template: string) {
const ast = parse(template)
const ast = baseParse(template)
transform(ast, {
nodeTransforms: [transformSrcset, transformElement],
directiveTransforms: {

View File

@@ -2,7 +2,9 @@ import {
CompilerOptions,
CodegenResult,
CompilerError,
NodeTransform
NodeTransform,
ParserOptions,
RootNode
} from '@vue/compiler-core'
import { SourceMapConsumer, SourceMapGenerator, RawSourceMap } from 'source-map'
import {
@@ -16,6 +18,7 @@ import consolidate from 'consolidate'
export interface TemplateCompiler {
compile(template: string, options: CompilerOptions): CodegenResult
parse(template: string, options: ParserOptions): RootNode
}
export interface SFCTemplateCompileResults {

View File

@@ -18,4 +18,8 @@ export {
SFCTemplateCompileResults
} from './compileTemplate'
export { SFCStyleCompileOptions, SFCStyleCompileResults } from './compileStyle'
export { CompilerOptions, generateCodeFrame } from '@vue/compiler-core'
export {
CompilerOptions,
CompilerError,
generateCodeFrame
} from '@vue/compiler-core'

View File

@@ -1,20 +1,20 @@
import {
parse as baseParse,
TextModes,
NodeTypes,
TextNode,
ElementNode,
SourceLocation
SourceLocation,
CompilerError
} from '@vue/compiler-core'
import { RawSourceMap, SourceMapGenerator } from 'source-map'
import LRUCache from 'lru-cache'
import { generateCodeFrame } from '@vue/shared'
import { TemplateCompiler } from './compileTemplate'
export interface SFCParseOptions {
filename?: string
sourceMap?: boolean
sourceRoot?: string
pad?: boolean | 'line' | 'space'
compiler?: TemplateCompiler
}
export interface SFCBlock {
@@ -50,24 +50,32 @@ export interface SFCDescriptor {
customBlocks: SFCBlock[]
}
export interface SFCParseResult {
descriptor: SFCDescriptor
errors: CompilerError[]
}
const SFC_CACHE_MAX_SIZE = 500
const sourceToSFC = new LRUCache<string, SFCDescriptor>(SFC_CACHE_MAX_SIZE)
const sourceToSFC = new LRUCache<string, SFCParseResult>(SFC_CACHE_MAX_SIZE)
export function parse(
source: string,
{
sourceMap = true,
filename = 'component.vue',
sourceRoot = '',
pad = false
pad = false,
compiler = require('@vue/compiler-dom')
}: SFCParseOptions = {}
): SFCDescriptor {
const sourceKey = source + sourceMap + filename + sourceRoot + pad
): SFCParseResult {
const sourceKey =
source + sourceMap + filename + sourceRoot + pad + compiler.parse
const cache = sourceToSFC.get(sourceKey)
if (cache) {
return cache
}
const sfc: SFCDescriptor = {
const descriptor: SFCDescriptor = {
filename,
template: null,
script: null,
@@ -75,9 +83,15 @@ export function parse(
customBlocks: []
}
const ast = baseParse(source, {
const errors: CompilerError[] = []
const ast = compiler.parse(source, {
// there are no components at SFC parsing level
isNativeTag: () => true,
getTextMode: () => TextModes.RAWTEXT
// preserve all whitespaces
isPreTag: () => true,
onError: e => {
errors.push(e)
}
})
ast.children.forEach(node => {
@@ -89,24 +103,28 @@ export function parse(
}
switch (node.tag) {
case 'template':
if (!sfc.template) {
sfc.template = createBlock(node, source, pad) as SFCTemplateBlock
if (!descriptor.template) {
descriptor.template = createBlock(
node,
source,
pad
) as SFCTemplateBlock
} else {
warnDuplicateBlock(source, filename, node)
}
break
case 'script':
if (!sfc.script) {
sfc.script = createBlock(node, source, pad) as SFCScriptBlock
if (!descriptor.script) {
descriptor.script = createBlock(node, source, pad) as SFCScriptBlock
} else {
warnDuplicateBlock(source, filename, node)
}
break
case 'style':
sfc.styles.push(createBlock(node, source, pad) as SFCStyleBlock)
descriptor.styles.push(createBlock(node, source, pad) as SFCStyleBlock)
break
default:
sfc.customBlocks.push(createBlock(node, source, pad))
descriptor.customBlocks.push(createBlock(node, source, pad))
break
}
})
@@ -123,13 +141,17 @@ export function parse(
)
}
}
genMap(sfc.template)
genMap(sfc.script)
sfc.styles.forEach(genMap)
genMap(descriptor.template)
genMap(descriptor.script)
descriptor.styles.forEach(genMap)
}
sourceToSFC.set(sourceKey, sfc)
return sfc
const result = {
descriptor,
errors
}
sourceToSFC.set(sourceKey, result)
return result
}
function warnDuplicateBlock(
@@ -156,12 +178,19 @@ function createBlock(
pad: SFCParseOptions['pad']
): SFCBlock {
const type = node.tag
const text = node.children[0] as TextNode
const start = node.children[0].loc.start
const end = node.children[node.children.length - 1].loc.end
const content = source.slice(start.offset, end.offset)
const loc = {
source: content,
start,
end
}
const attrs: Record<string, string | true> = {}
const block: SFCBlock = {
type,
content: text.content,
loc: text.loc,
content,
loc,
attrs
}
if (node.tag !== 'template' && pad) {