wip: support reusing template ast from sfc descriptor

This commit is contained in:
Evan You 2023-11-20 22:05:27 +08:00
parent fc4f801070
commit 5fc695be26
6 changed files with 89 additions and 33 deletions

View File

@ -38,11 +38,11 @@ export const DOMDirectiveTransforms: Record<string, DirectiveTransform> = {
} }
export function compile( export function compile(
template: string, src: string | RootNode,
options: CompilerOptions = {} options: CompilerOptions = {}
): CodegenResult { ): CodegenResult {
return baseCompile( return baseCompile(
template, src,
extend({}, parserOptions, options, { extend({}, parserOptions, options, {
nodeTransforms: [ nodeTransforms: [
// ignore <script> and <tag> // ignore <script> and <tag>

View File

@ -58,24 +58,6 @@ export function ssrRender(_ctx, _push, _parent, _attrs) {
}" }"
`; `;
exports[`source map 1`] = `
{
"mappings": ";;;wBACE,oBAA8B;IAAzB,oBAAmB,4BAAbA,WAAM",
"names": [
"render",
],
"sources": [
"example.vue",
],
"sourcesContent": [
"
<div><p>{{ render }}</p></div>
",
],
"version": 3,
}
`;
exports[`template errors 1`] = ` exports[`template errors 1`] = `
[ [
[SyntaxError: Error parsing JavaScript expression: Unexpected token (1:3)], [SyntaxError: Error parsing JavaScript expression: Unexpected token (1:3)],

View File

@ -1,3 +1,4 @@
import { RawSourceMap, SourceMapConsumer } from 'source-map-js'
import { import {
compileTemplate, compileTemplate,
SFCTemplateCompileOptions SFCTemplateCompileOptions
@ -107,18 +108,54 @@ test('source map', () => {
const template = parse( const template = parse(
` `
<template> <template>
<div><p>{{ render }}</p></div> <div><p>{{ foobar }}</p></div>
</template> </template>
`, `,
{ filename: 'example.vue', sourceMap: true } { filename: 'example.vue', sourceMap: true }
).descriptor.template as SFCTemplateBlock ).descriptor.template!
const result = compile({ const { code, map } = compile({
filename: 'example.vue', filename: 'example.vue',
source: template.content source: template.content
}) })
expect(result.map).toMatchSnapshot() expect(map!.sources).toEqual([`example.vue`])
expect(map!.sourcesContent).toEqual([template.content])
const consumer = new SourceMapConsumer(map as RawSourceMap)
expect(
consumer.originalPositionFor(getPositionInCode(code, 'foobar'))
).toMatchObject(getPositionInCode(template.content, `foobar`))
})
test('should work w/ AST from descriptor', () => {
const source = `
<template>
<div><p>{{ foobar }}</p></div>
</template>
`
const template = parse(source, {
filename: 'example.vue',
sourceMap: true
}).descriptor.template!
expect(template.ast.source).toBe(source)
const { code, map } = compile({
filename: 'example.vue',
source: template.content,
ast: template.ast
})
expect(map!.sources).toEqual([`example.vue`])
// when reusing AST from SFC parse for template compile,
// the source corresponds to the entire SFC
expect(map!.sourcesContent).toEqual([source])
const consumer = new SourceMapConsumer(map as RawSourceMap)
expect(
consumer.originalPositionFor(getPositionInCode(code, 'foobar'))
).toMatchObject(getPositionInCode(source, `foobar`))
}) })
test('template errors', () => { test('template errors', () => {
@ -199,3 +236,36 @@ test('dynamic v-on + static v-on should merged', () => {
expect(result.code).toMatchSnapshot() expect(result.code).toMatchSnapshot()
}) })
interface Pos {
line: number
column: number
name?: string
}
function getPositionInCode(
code: string,
token: string,
expectName: string | boolean = false
): Pos {
const generatedOffset = code.indexOf(token)
let line = 1
let lastNewLinePos = -1
for (let i = 0; i < generatedOffset; i++) {
if (code.charCodeAt(i) === 10 /* newline char code */) {
line++
lastNewLinePos = i
}
}
const res: Pos = {
line,
column:
lastNewLinePos === -1
? generatedOffset
: generatedOffset - lastNewLinePos - 1
}
if (expectName) {
res.name = typeof expectName === 'string' ? expectName : token
}
return res
}

View File

@ -30,7 +30,7 @@ import { warnOnce } from './warn'
import { genCssVarsFromList } from './style/cssVars' import { genCssVarsFromList } from './style/cssVars'
export interface TemplateCompiler { export interface TemplateCompiler {
compile(template: string, options: CompilerOptions): CodegenResult compile(source: string | RootNode, options: CompilerOptions): CodegenResult
parse(template: string, options: ParserOptions): RootNode parse(template: string, options: ParserOptions): RootNode
} }
@ -46,6 +46,7 @@ export interface SFCTemplateCompileResults {
export interface SFCTemplateCompileOptions { export interface SFCTemplateCompileOptions {
source: string source: string
ast?: RootNode
filename: string filename: string
id: string id: string
scoped?: boolean scoped?: boolean
@ -164,6 +165,7 @@ function doCompileTemplate({
slotted, slotted,
inMap, inMap,
source, source,
ast: inAST,
ssr = false, ssr = false,
ssrCssVars, ssrCssVars,
isProd = false, isProd = false,
@ -199,7 +201,7 @@ function doCompileTemplate({
const shortId = id.replace(/^data-v-/, '') const shortId = id.replace(/^data-v-/, '')
const longId = `data-v-${shortId}` const longId = `data-v-${shortId}`
let { code, ast, preamble, map } = compiler.compile(source, { let { code, ast, preamble, map } = compiler.compile(inAST || source, {
mode: 'module', mode: 'module',
prefixIdentifiers: true, prefixIdentifiers: true,
hoistStatic: true, hoistStatic: true,
@ -235,7 +237,7 @@ function doCompileTemplate({
let msg = w.message let msg = w.message
if (w.loc) { if (w.loc) {
msg += `\n${generateCodeFrame( msg += `\n${generateCodeFrame(
source, inAST?.source || source,
w.loc.start.offset, w.loc.start.offset,
w.loc.end.offset w.loc.end.offset
)}` )}`

View File

@ -3,7 +3,9 @@ import {
ElementNode, ElementNode,
SourceLocation, SourceLocation,
CompilerError, CompilerError,
BindingMetadata BindingMetadata,
RootNode,
createRoot
} from '@vue/compiler-core' } from '@vue/compiler-core'
import * as CompilerDOM from '@vue/compiler-dom' import * as CompilerDOM from '@vue/compiler-dom'
import { RawSourceMap, SourceMapGenerator } from 'source-map-js' import { RawSourceMap, SourceMapGenerator } from 'source-map-js'
@ -36,7 +38,7 @@ export interface SFCBlock {
export interface SFCTemplateBlock extends SFCBlock { export interface SFCTemplateBlock extends SFCBlock {
type: 'template' type: 'template'
ast: ElementNode ast: RootNode
} }
export interface SFCScriptBlock extends SFCBlock { export interface SFCScriptBlock extends SFCBlock {
@ -154,7 +156,7 @@ export function parse(
source, source,
false false
) as SFCTemplateBlock) ) as SFCTemplateBlock)
templateBlock.ast = node templateBlock.ast = createRoot(node.children, source)
// warn against 2.x <template functional> // warn against 2.x <template functional>
if (templateBlock.attrs.functional) { if (templateBlock.attrs.functional) {
@ -243,7 +245,8 @@ export function parse(
) )
} }
} }
genMap(descriptor.template) // no need to genMap for template as its AST already accounts for the
// position in the SFC
genMap(descriptor.script) genMap(descriptor.script)
descriptor.styles.forEach(genMap) descriptor.styles.forEach(genMap)
descriptor.customBlocks.forEach(genMap) descriptor.customBlocks.forEach(genMap)

View File

@ -3,7 +3,6 @@ import { SFCDescriptor } from '../parse'
import { import {
NodeTypes, NodeTypes,
SimpleExpressionNode, SimpleExpressionNode,
createRoot,
forAliasRE, forAliasRE,
parserOptions, parserOptions,
transform, transform,
@ -35,7 +34,7 @@ function resolveTemplateUsageCheckString(sfc: SFCDescriptor) {
} }
let code = '' let code = ''
transform(createRoot([ast]), { transform(ast, {
nodeTransforms: [ nodeTransforms: [
node => { node => {
if (node.type === NodeTypes.ELEMENT) { if (node.type === NodeTypes.ELEMENT) {