diff --git a/packages/compiler-core/src/codegen.ts b/packages/compiler-core/src/codegen.ts index 4f9290be..41f6e4c7 100644 --- a/packages/compiler-core/src/codegen.ts +++ b/packages/compiler-core/src/codegen.ts @@ -79,6 +79,8 @@ function createCodegenContext( sourceMap = false, filename = `template.vue.html`, scopeId = null, + runtimeGlobalName = `Vue`, + runtimeModuleName = `vue`, ssr = false }: CodegenOptions ): CodegenContext { @@ -88,6 +90,8 @@ function createCodegenContext( sourceMap, filename, scopeId, + runtimeGlobalName, + runtimeModuleName, ssr, source: ast.loc.source, code: ``, @@ -275,8 +279,18 @@ export function generate( } function genFunctionPreamble(ast: RootNode, context: CodegenContext) { - const { ssr, helper, prefixIdentifiers, push, newline } = context - const VueBinding = ssr ? `require("vue")` : `Vue` + const { + ssr, + helper, + prefixIdentifiers, + push, + newline, + runtimeModuleName, + runtimeGlobalName + } = context + const VueBinding = ssr + ? `require(${JSON.stringify(runtimeModuleName)})` + : runtimeGlobalName // Generate const declaration for helpers // In prefix mode, we place the const declaration at top so it's done // only once; But if we not prefixing, we place the declaration inside the @@ -319,7 +333,7 @@ function genModulePreamble( context: CodegenContext, genScopeId: boolean ) { - const { push, helper, newline, scopeId } = context + const { push, helper, newline, scopeId, runtimeModuleName } = context // generate import statements for helpers if (genScopeId) { ast.helpers.push(WITH_SCOPE_ID) @@ -328,7 +342,11 @@ function genModulePreamble( } } if (ast.helpers.length) { - push(`import { ${ast.helpers.map(helper).join(', ')} } from "vue"\n`) + push( + `import { ${ast.helpers.map(helper).join(', ')} } from ${JSON.stringify( + runtimeModuleName + )}\n` + ) } if (!__BROWSER__ && ast.ssrHelpers && ast.ssrHelpers.length) { push( diff --git a/packages/compiler-core/src/options.ts b/packages/compiler-core/src/options.ts index f108edcf..d195cf39 100644 --- a/packages/compiler-core/src/options.ts +++ b/packages/compiler-core/src/options.ts @@ -71,6 +71,9 @@ export interface CodegenOptions { scopeId?: string | null // we need to know about this to generate proper preambles prefixIdentifiers?: boolean + // for specifying where to import helpers + runtimeModuleName?: string + runtimeGlobalName?: string // generate ssr-specific code? ssr?: boolean } diff --git a/packages/compiler-core/src/transforms/transformElement.ts b/packages/compiler-core/src/transforms/transformElement.ts index 1473f428..7c9dd0fe 100644 --- a/packages/compiler-core/src/transforms/transformElement.ts +++ b/packages/compiler-core/src/transforms/transformElement.ts @@ -182,7 +182,8 @@ export const transformElement: NodeTransform = (node, context) => { export function resolveComponentType( node: ComponentNode, - context: TransformContext + context: TransformContext, + ssr = false ) { const { tag } = node @@ -211,7 +212,9 @@ export function resolveComponentType( // 2. built-in components (Portal, Transition, KeepAlive, Suspense...) const builtIn = isCoreComponent(tag) || context.isBuiltInComponent(tag) if (builtIn) { - context.helper(builtIn) + // built-ins are simply fallthroughs / have special handling during ssr + // no we don't need to import their runtime equivalents + if (!ssr) context.helper(builtIn) return builtIn } diff --git a/packages/compiler-dom/src/index.ts b/packages/compiler-dom/src/index.ts index 6cecb8b2..5e1d17df 100644 --- a/packages/compiler-dom/src/index.ts +++ b/packages/compiler-dom/src/index.ts @@ -22,6 +22,14 @@ export const parserOptions = __BROWSER__ ? parserOptionsMinimal : parserOptionsStandard +export const isBuiltInDOMComponent = (tag: string): symbol | undefined => { + if (isBuiltInType(tag, `Transition`)) { + return TRANSITION + } else if (isBuiltInType(tag, `TransitionGroup`)) { + return TRANSITION_GROUP + } +} + export function compile( template: string, options: CompilerOptions = {} @@ -39,13 +47,7 @@ export function compile( show: transformShow, ...(options.directiveTransforms || {}) }, - isBuiltInComponent: tag => { - if (isBuiltInType(tag, `Transition`)) { - return TRANSITION - } else if (isBuiltInType(tag, `TransitionGroup`)) { - return TRANSITION_GROUP - } - } + isBuiltInComponent: isBuiltInDOMComponent }) } @@ -56,6 +58,7 @@ export function parse(template: string, options: ParserOptions = {}): RootNode { }) } +export * from './runtimeHelpers' export { transformStyle } from './transforms/transformStyle' export { createDOMCompilerError, DOMErrorCodes } from './errors' export * from '@vue/compiler-core' diff --git a/packages/compiler-ssr/__tests__/ssrComponent.spec.ts b/packages/compiler-ssr/__tests__/ssrComponent.spec.ts index 1f24a924..3fd0629f 100644 --- a/packages/compiler-ssr/__tests__/ssrComponent.spec.ts +++ b/packages/compiler-ssr/__tests__/ssrComponent.spec.ts @@ -161,5 +161,47 @@ describe('ssr: components', () => { }" `) }) + + test('built-in fallthroughs', () => { + // no fragment + expect(compile(`
`).code) + .toMatchInlineSnapshot(` + " + return function ssrRender(_ctx, _push, _parent) { + _push(\`
\`) + }" + `) + + // wrap with fragment + expect(compile(`
`).code) + .toMatchInlineSnapshot(` + " + return function ssrRender(_ctx, _push, _parent) { + _push(\`
\`) + }" + `) + + // no fragment + expect(compile(``).code) + .toMatchInlineSnapshot(` + "const { resolveComponent } = require(\\"vue\\") + const { _ssrRenderComponent } = require(\\"@vue/server-renderer\\") + + return function ssrRender(_ctx, _push, _parent) { + const _component_foo = resolveComponent(\\"foo\\") + + _ssrRenderComponent(_component_foo, null, null, _parent) + }" + `) + + // wrap with fragment + expect(compile(`
`).code) + .toMatchInlineSnapshot(` + " + return function ssrRender(_ctx, _push, _parent) { + _push(\`
\`) + }" + `) + }) }) }) diff --git a/packages/compiler-ssr/src/index.ts b/packages/compiler-ssr/src/index.ts index e018e2d9..2a0ba788 100644 --- a/packages/compiler-ssr/src/index.ts +++ b/packages/compiler-ssr/src/index.ts @@ -10,7 +10,8 @@ import { trackSlotScopes, noopDirectiveTransform, transformBind, - transformStyle + transformStyle, + isBuiltInDOMComponent } from '@vue/compiler-dom' import { ssrCodegenTransform } from './ssrCodegenTransform' import { ssrTransformElement } from './transforms/ssrTransformElement' @@ -64,7 +65,8 @@ export function compile( cloak: noopDirectiveTransform, once: noopDirectiveTransform, ...(options.directiveTransforms || {}) // user transforms - } + }, + isBuiltInComponent: isBuiltInDOMComponent }) // traverse the template AST and convert into SSR codegen AST diff --git a/packages/compiler-ssr/src/ssrCodegenTransform.ts b/packages/compiler-ssr/src/ssrCodegenTransform.ts index ed5cc103..d9efb3b0 100644 --- a/packages/compiler-ssr/src/ssrCodegenTransform.ts +++ b/packages/compiler-ssr/src/ssrCodegenTransform.ts @@ -28,17 +28,9 @@ import { ssrProcessComponent } from './transforms/ssrTransformComponent' export function ssrCodegenTransform(ast: RootNode, options: CompilerOptions) { const context = createSSRTransformContext(options) - const isFragment = ast.children.length > 1 && !ast.children.every(c => isText(c)) - if (isFragment) { - context.pushStringPart(``) - } - processChildren(ast.children, context) - if (isFragment) { - context.pushStringPart(``) - } - + processChildren(ast.children, context, isFragment) ast.codegenNode = createBlockStatement(context.body) // Finalize helpers. @@ -99,8 +91,12 @@ export function createChildContext( export function processChildren( children: TemplateChildNode[], - context: SSRTransformContext + context: SSRTransformContext, + asFragment = false ) { + if (asFragment) { + context.pushStringPart(``) + } const isVoidTag = context.options.isVoidTag || NO for (let i = 0; i < children.length; i++) { const child = children[i] @@ -135,4 +131,7 @@ export function processChildren( ssrProcessFor(child, context) } } + if (asFragment) { + context.pushStringPart(``) + } } diff --git a/packages/compiler-ssr/src/transforms/ssrTransformComponent.ts b/packages/compiler-ssr/src/transforms/ssrTransformComponent.ts index 6916515d..de055f3d 100644 --- a/packages/compiler-ssr/src/transforms/ssrTransformComponent.ts +++ b/packages/compiler-ssr/src/transforms/ssrTransformComponent.ts @@ -6,14 +6,15 @@ import { resolveComponentType, buildProps, ComponentNode, - PORTAL, - SUSPENSE, SlotFnBuilder, createFunctionExpression, createBlockStatement, buildSlots, FunctionExpression, - TemplateChildNode + TemplateChildNode, + PORTAL, + SUSPENSE, + TRANSITION_GROUP } from '@vue/compiler-dom' import { SSR_RENDER_COMPONENT } from '../runtimeHelpers' import { @@ -34,6 +35,8 @@ interface WIPSlotEntry { children: TemplateChildNode[] } +const componentTypeMap = new WeakMap() + export const ssrTransformComponent: NodeTransform = (node, context) => { if ( node.type !== NodeTypes.ELEMENT || @@ -43,18 +46,10 @@ export const ssrTransformComponent: NodeTransform = (node, context) => { } return function ssrPostTransformComponent() { - const component = resolveComponentType(node, context) - + const component = resolveComponentType(node, context, true /* ssr */) if (isSymbol(component)) { - // built-in compoonent - if (component === PORTAL) { - // TODO - } else if (component === SUSPENSE) { - // TODO fallthrough - // TODO option to use fallback content and resolve on client - } else { - // TODO fallthrough for KeepAlive & Transition - } + componentTypeMap.set(node, component) + return // built-in component: fallthrough } // note we are not passing ssr: true here because for components, v-on @@ -98,13 +93,28 @@ export function ssrProcessComponent( node: ComponentNode, context: SSRTransformContext ) { - // finish up slot function expressions from the 1st pass. - const wipEntries = wipMap.get(node) || [] - for (let i = 0; i < wipEntries.length; i++) { - const { fn, children } = wipEntries[i] - const childContext = createChildContext(context) - processChildren(children, childContext) - fn.body = createBlockStatement(childContext.body) + if (!node.ssrCodegenNode) { + // this is a built-in component that fell-through. + // just render its children. + const component = componentTypeMap.get(node)! + + if (component === PORTAL) { + // TODO + return + } + + const needFragmentWrapper = + component === SUSPENSE || component === TRANSITION_GROUP + processChildren(node.children, context, needFragmentWrapper) + } else { + // finish up slot function expressions from the 1st pass. + const wipEntries = wipMap.get(node) || [] + for (let i = 0; i < wipEntries.length; i++) { + const { fn, children } = wipEntries[i] + const childContext = createChildContext(context) + processChildren(children, childContext) + fn.body = createBlockStatement(childContext.body) + } + context.pushStatement(node.ssrCodegenNode) } - context.pushStatement(node.ssrCodegenNode!) } diff --git a/packages/compiler-ssr/src/transforms/ssrVFor.ts b/packages/compiler-ssr/src/transforms/ssrVFor.ts index effa8d88..ec9f251e 100644 --- a/packages/compiler-ssr/src/transforms/ssrVFor.ts +++ b/packages/compiler-ssr/src/transforms/ssrVFor.ts @@ -27,13 +27,7 @@ export function ssrProcessFor(node: ForNode, context: SSRTransformContext) { const childContext = createChildContext(context) const needFragmentWrapper = node.children.length !== 1 || node.children[0].type !== NodeTypes.ELEMENT - if (needFragmentWrapper) { - childContext.pushStringPart(``) - } - processChildren(node.children, childContext) - if (needFragmentWrapper) { - childContext.pushStringPart(``) - } + processChildren(node.children, childContext, needFragmentWrapper) const renderLoop = createFunctionExpression( createForLoopParams(node.parseResult) ) diff --git a/packages/compiler-ssr/src/transforms/ssrVIf.ts b/packages/compiler-ssr/src/transforms/ssrVIf.ts index 23e06db2..a642b9fd 100644 --- a/packages/compiler-ssr/src/transforms/ssrVIf.ts +++ b/packages/compiler-ssr/src/transforms/ssrVIf.ts @@ -64,12 +64,6 @@ function processIfBranch( // optimize away nested fragments when the only child is a ForNode !(children.length === 1 && children[0].type === NodeTypes.FOR) const childContext = createChildContext(context) - if (needFragmentWrapper) { - childContext.pushStringPart(``) - } - processChildren(children, childContext) - if (needFragmentWrapper) { - childContext.pushStringPart(``) - } + processChildren(children, childContext, needFragmentWrapper) return createBlockStatement(childContext.body) }