refactor: expose parse in compiler-dom, improve sfc parse error handling
This commit is contained in:
@@ -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',
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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: {
|
||||
|
||||
@@ -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: {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user