diff --git a/packages/compiler-core/src/ast.ts b/packages/compiler-core/src/ast.ts index 117fef835aa..05c187feec0 100644 --- a/packages/compiler-core/src/ast.ts +++ b/packages/compiler-core/src/ast.ts @@ -113,6 +113,7 @@ export interface RootNode extends Node { temps: number ssrHelpers?: symbol[] codegenNode?: TemplateChildNode | JSChildNode | BlockStatement + transformed?: boolean // v2 compat only filters?: string[] diff --git a/packages/compiler-core/src/transform.ts b/packages/compiler-core/src/transform.ts index 2053e26ce92..f128b3b6e9d 100644 --- a/packages/compiler-core/src/transform.ts +++ b/packages/compiler-core/src/transform.ts @@ -334,6 +334,7 @@ export function transform(root: RootNode, options: TransformOptions) { root.hoists = context.hoists root.temps = context.temps root.cached = context.cached + root.transformed = true if (__COMPAT__) { root.filters = [...context.filters!] diff --git a/packages/compiler-sfc/__tests__/compileTemplate.spec.ts b/packages/compiler-sfc/__tests__/compileTemplate.spec.ts index 3b74dd66ada..29be21da89a 100644 --- a/packages/compiler-sfc/__tests__/compileTemplate.spec.ts +++ b/packages/compiler-sfc/__tests__/compileTemplate.spec.ts @@ -156,6 +156,52 @@ test('should work w/ AST from descriptor', () => { expect( consumer.originalPositionFor(getPositionInCode(code, 'foobar')) ).toMatchObject(getPositionInCode(source, `foobar`)) + + expect(code).toBe( + compile({ + filename: 'example.vue', + source: template.content + }).code + ) +}) + +test('should work w/ AST from descriptor in SSR mode', () => { + const source = ` + + ` + 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: '', // make sure it's actually using the AST instead of source + ast: template.ast, + ssr: true + }) + + 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`)) + + expect(code).toBe( + compile({ + filename: 'example.vue', + source: template.content, + ssr: true + }).code + ) }) test('should not reuse AST if using custom compiler', () => { @@ -185,6 +231,66 @@ test('should not reuse AST if using custom compiler', () => { expect(code).toBe(template.content) }) +test('should force re-parse on already transformed AST', () => { + const source = ` + + ` + const template = parse(source, { + filename: 'example.vue', + sourceMap: true + }).descriptor.template! + + // force set to empty, if this is reused then it won't generate proper code + template.ast!.children = [] + template.ast!.transformed = true + + const { code } = compile({ + filename: 'example.vue', + source: '', + ast: template.ast + }) + + expect(code).toBe( + compile({ + filename: 'example.vue', + source: template.content + }).code + ) +}) + +test('should force re-parse with correct compiler in SSR mode', () => { + const source = ` + + ` + const template = parse(source, { + filename: 'example.vue', + sourceMap: true + }).descriptor.template! + + // force set to empty, if this is reused then it won't generate proper code + template.ast!.children = [] + template.ast!.transformed = true + + const { code } = compile({ + filename: 'example.vue', + source: '', + ast: template.ast, + ssr: true + }) + + expect(code).toBe( + compile({ + filename: 'example.vue', + source: template.content, + ssr: true + }).code + ) +}) + test('template errors', () => { const result = compile({ filename: 'example.vue', diff --git a/packages/compiler-sfc/src/compileTemplate.ts b/packages/compiler-sfc/src/compileTemplate.ts index 1afdc74bf22..e5f38c5baca 100644 --- a/packages/compiler-sfc/src/compileTemplate.ts +++ b/packages/compiler-sfc/src/compileTemplate.ts @@ -214,10 +214,10 @@ function doCompileTemplate({ inAST = undefined } - if (inAST?.codegenNode) { - // input AST has codegenNode - it has already been transformed and cannot - // be reused. We need to parse a fresh one. Can't just use `source` here - // since we need the AST location info to be relative to the entire SFC. + if (inAST?.transformed) { + // If input AST has already been transformed, then it cannot be reused. + // We need to parse a fresh one. Can't just use `source` here since we need + // the AST location info to be relative to the entire SFC. const newAST = (ssr ? CompilerDOM : compiler).parse(inAST.source, { parseMode: 'sfc', onError: e => errors.push(e) diff --git a/packages/compiler-ssr/src/index.ts b/packages/compiler-ssr/src/index.ts index 1e5f9055064..cae8cf711a8 100644 --- a/packages/compiler-ssr/src/index.ts +++ b/packages/compiler-ssr/src/index.ts @@ -11,7 +11,8 @@ import { noopDirectiveTransform, transformBind, transformStyle, - transformOn + transformOn, + RootNode } from '@vue/compiler-dom' import { ssrCodegenTransform } from './ssrCodegenTransform' import { ssrTransformElement } from './transforms/ssrTransformElement' @@ -28,12 +29,11 @@ import { ssrInjectFallthroughAttrs } from './transforms/ssrInjectFallthroughAttr import { ssrInjectCssVars } from './transforms/ssrInjectCssVars' export function compile( - template: string, + source: string | RootNode, options: CompilerOptions = {} ): CodegenResult { options = { ...options, - // apply DOM-specific parsing options ...parserOptions, ssr: true, inSSR: true, @@ -45,7 +45,7 @@ export function compile( hoistStatic: false } - const ast = baseParse(template, options) + const ast = typeof source === 'string' ? baseParse(source, options) : source // Save raw options for AST. This is needed when performing sub-transforms // on slot vnode branches.