wip(ssr): ssr helper codegen

This commit is contained in:
Evan You 2020-02-03 17:47:06 -05:00
parent d1d81cf1f9
commit b685805a26
18 changed files with 374 additions and 253 deletions

View File

@ -103,6 +103,7 @@ export interface RootNode extends Node {
imports: ImportItem[] imports: ImportItem[]
cached: number cached: number
codegenNode?: TemplateChildNode | JSChildNode | BlockStatement | undefined codegenNode?: TemplateChildNode | JSChildNode | BlockStatement | undefined
ssrHelpers?: symbol[]
} }
export type ElementNode = export type ElementNode =

View File

@ -74,7 +74,7 @@ function createCodegenContext(
ast: RootNode, ast: RootNode,
{ {
mode = 'function', mode = 'function',
prefixIdentifiers = mode === 'module' || mode === 'cjs', prefixIdentifiers = mode === 'module',
sourceMap = false, sourceMap = false,
filename = `template.vue.html`, filename = `template.vue.html`,
scopeId = null, scopeId = null,
@ -169,7 +169,6 @@ export function generate(
const { const {
mode, mode,
push, push,
helper,
prefixIdentifiers, prefixIdentifiers,
indent, indent,
deindent, deindent,
@ -182,58 +181,10 @@ export function generate(
const genScopeId = !__BROWSER__ && scopeId != null && mode === 'module' const genScopeId = !__BROWSER__ && scopeId != null && mode === 'module'
// preambles // preambles
if (mode === 'function' || mode === 'cjs') { if (mode === 'module') {
const VueBinding = mode === 'function' ? `Vue` : `require("vue")` genModulePreamble(ast, context, genScopeId)
// 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
// with block so it doesn't incur the `in` check cost for every helper access.
if (hasHelpers) {
if (prefixIdentifiers) {
push(
`const { ${ast.helpers.map(helper).join(', ')} } = ${VueBinding}\n`
)
} else {
// "with" mode.
// save Vue in a separate variable to avoid collision
push(`const _Vue = ${VueBinding}\n`)
// in "with" mode, helpers are declared inside the with block to avoid
// has check cost, but hoists are lifted out of the function - we need
// to provide the helper here.
if (ast.hoists.length) {
const staticHelpers = [CREATE_VNODE, CREATE_COMMENT, CREATE_TEXT]
.filter(helper => ast.helpers.includes(helper))
.map(s => `${helperNameMap[s]}: _${helperNameMap[s]}`)
.join(', ')
push(`const { ${staticHelpers} } = _Vue\n`)
}
}
}
genHoists(ast.hoists, context)
newline()
push(`return `)
} else { } else {
// generate import statements for helpers genFunctionPreamble(ast, context)
if (genScopeId) {
ast.helpers.push(WITH_SCOPE_ID)
if (ast.hoists.length) {
ast.helpers.push(PUSH_SCOPE_ID, POP_SCOPE_ID)
}
}
if (hasHelpers) {
push(`import { ${ast.helpers.map(helper).join(', ')} } from "vue"\n`)
}
if (ast.imports.length) {
genImports(ast.imports, context)
newline()
}
if (genScopeId) {
push(`const withId = ${helper(WITH_SCOPE_ID)}("${scopeId}")`)
newline()
}
genHoists(ast.hoists, context)
newline()
push(`export `)
} }
// enter render function // enter render function
@ -315,6 +266,82 @@ export function generate(
} }
} }
function genFunctionPreamble(ast: RootNode, context: CodegenContext) {
const { mode, helper, prefixIdentifiers, push, newline } = context
const VueBinding = mode === 'function' ? `Vue` : `require("vue")`
// 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
// with block so it doesn't incur the `in` check cost for every helper access.
if (ast.helpers.length > 0) {
if (prefixIdentifiers) {
push(`const { ${ast.helpers.map(helper).join(', ')} } = ${VueBinding}\n`)
} else {
// "with" mode.
// save Vue in a separate variable to avoid collision
push(`const _Vue = ${VueBinding}\n`)
// in "with" mode, helpers are declared inside the with block to avoid
// has check cost, but hoists are lifted out of the function - we need
// to provide the helper here.
if (ast.hoists.length) {
const staticHelpers = [CREATE_VNODE, CREATE_COMMENT, CREATE_TEXT]
.filter(helper => ast.helpers.includes(helper))
.map(s => `${helperNameMap[s]}: _${helperNameMap[s]}`)
.join(', ')
push(`const { ${staticHelpers} } = _Vue\n`)
}
}
}
// generate variables for ssr helpers
if (!__BROWSER__ && ast.ssrHelpers && ast.ssrHelpers.length) {
// ssr guaruntees prefixIdentifier: true
push(
`const { ${ast.ssrHelpers
.map(helper)
.join(', ')} } = require("@vue/server-renderer")\n`
)
}
genHoists(ast.hoists, context)
newline()
push(`return `)
}
function genModulePreamble(
ast: RootNode,
context: CodegenContext,
genScopeId: boolean
) {
const { push, helper, newline, scopeId } = context
// generate import statements for helpers
if (genScopeId) {
ast.helpers.push(WITH_SCOPE_ID)
if (ast.hoists.length) {
ast.helpers.push(PUSH_SCOPE_ID, POP_SCOPE_ID)
}
}
if (ast.helpers.length) {
push(`import { ${ast.helpers.map(helper).join(', ')} } from "vue"\n`)
}
if (!__BROWSER__ && ast.ssrHelpers && ast.ssrHelpers.length) {
push(
`import { ${ast.ssrHelpers
.map(helper)
.join(', ')} } from "@vue/server-renderer"\n`
)
}
if (ast.imports.length) {
genImports(ast.imports, context)
newline()
}
if (genScopeId) {
push(`const withId = ${helper(WITH_SCOPE_ID)}("${scopeId}")`)
newline()
}
genHoists(ast.hoists, context)
newline()
push(`export `)
}
function genAssets( function genAssets(
assets: string[], assets: string[],
type: 'component' | 'directive', type: 'component' | 'directive',

View File

@ -33,6 +33,7 @@ export { transformOn } from './transforms/vOn'
// exported for compiler-ssr // exported for compiler-ssr
export { processIfBranches } from './transforms/vIf' export { processIfBranches } from './transforms/vIf'
export { processForNode } from './transforms/vFor'
export { export {
transformExpression, transformExpression,
processExpression processExpression

View File

@ -23,7 +23,6 @@ export interface ParserOptions {
// this number is based on the map above, but it should be pre-computed // this number is based on the map above, but it should be pre-computed
// to avoid the cost on every parse() call. // to avoid the cost on every parse() call.
maxCRNameLength?: number maxCRNameLength?: number
onError?: (error: CompilerError) => void onError?: (error: CompilerError) => void
} }
@ -32,6 +31,8 @@ export interface TransformOptions {
directiveTransforms?: { [name: string]: DirectiveTransform | undefined } directiveTransforms?: { [name: string]: DirectiveTransform | undefined }
isBuiltInComponent?: (tag: string) => symbol | void isBuiltInComponent?: (tag: string) => symbol | void
// Transform expressions like {{ foo }} to `_ctx.foo`. // Transform expressions like {{ foo }} to `_ctx.foo`.
// If this option is false, the generated code will be wrapped in a
// `with (this) { ... }` block.
// - This is force-enabled in module mode, since modules are by default strict // - This is force-enabled in module mode, since modules are by default strict
// and cannot use `with` // and cannot use `with`
// - Default: mode === 'module' // - Default: mode === 'module'
@ -48,6 +49,7 @@ export interface TransformOptions {
// analysis to determine if a handler is safe to cache. // analysis to determine if a handler is safe to cache.
// - Default: false // - Default: false
cacheHandlers?: boolean cacheHandlers?: boolean
ssr?: boolean
onError?: (error: CompilerError) => void onError?: (error: CompilerError) => void
} }
@ -61,13 +63,6 @@ export interface CodegenOptions {
// `require('vue')`. // `require('vue')`.
// - Default: 'function' // - Default: 'function'
mode?: 'module' | 'function' | 'cjs' mode?: 'module' | 'function' | 'cjs'
// Prefix suitable identifiers with _ctx.
// If this option is false, the generated code will be wrapped in a
// `with (this) { ... }` block.
// - This is force-enabled in module mode, since modules are by default strict
// and cannot use `with`
// - Default: mode === 'module'
prefixIdentifiers?: boolean
// Generate source map? // Generate source map?
// - Default: false // - Default: false
sourceMap?: boolean sourceMap?: boolean
@ -76,7 +71,9 @@ export interface CodegenOptions {
filename?: string filename?: string
// SFC scoped styles ID // SFC scoped styles ID
scopeId?: string | null scopeId?: string | null
// generate SSR specific code? // we need to know about this to generate proper preambles
prefixIdentifiers?: boolean
// generate ssr-specific code?
ssr?: boolean ssr?: boolean
} }

View File

@ -115,6 +115,7 @@ function createTransformContext(
nodeTransforms = [], nodeTransforms = [],
directiveTransforms = {}, directiveTransforms = {},
isBuiltInComponent = NOOP, isBuiltInComponent = NOOP,
ssr = false,
onError = defaultOnError onError = defaultOnError
}: TransformOptions }: TransformOptions
): TransformContext { ): TransformContext {
@ -126,6 +127,7 @@ function createTransformContext(
nodeTransforms, nodeTransforms,
directiveTransforms, directiveTransforms,
isBuiltInComponent, isBuiltInComponent,
ssr,
onError, onError,
// state // state
@ -256,10 +258,19 @@ export function transform(root: RootNode, options: TransformOptions) {
if (options.hoistStatic) { if (options.hoistStatic) {
hoistStatic(root, context) hoistStatic(root, context)
} }
finalizeRoot(root, context) if (!options.ssr) {
createRootCodegen(root, context)
}
// finalize meta information
root.helpers = [...context.helpers]
root.components = [...context.components]
root.directives = [...context.directives]
root.imports = [...context.imports]
root.hoists = context.hoists
root.cached = context.cached
} }
function finalizeRoot(root: RootNode, context: TransformContext) { function createRootCodegen(root: RootNode, context: TransformContext) {
const { helper } = context const { helper } = context
const { children } = root const { children } = root
const child = children[0] const child = children[0]
@ -304,13 +315,6 @@ function finalizeRoot(root: RootNode, context: TransformContext) {
} else { } else {
// no children = noop. codegen will return null. // no children = noop. codegen will return null.
} }
// finalize meta information
root.helpers = [...context.helpers]
root.components = [...context.components]
root.directives = [...context.directives]
root.imports = [...context.imports]
root.hoists = context.hoists
root.cached = context.cached
} }
export function traverseChildren( export function traverseChildren(
@ -359,13 +363,17 @@ export function traverseNode(
switch (node.type) { switch (node.type) {
case NodeTypes.COMMENT: case NodeTypes.COMMENT:
// inject import for the Comment symbol, which is needed for creating if (!context.ssr) {
// comment nodes with `createVNode` // inject import for the Comment symbol, which is needed for creating
context.helper(CREATE_COMMENT) // comment nodes with `createVNode`
context.helper(CREATE_COMMENT)
}
break break
case NodeTypes.INTERPOLATION: case NodeTypes.INTERPOLATION:
// no need to traverse, but we need to inject toString helper // no need to traverse, but we need to inject toString helper
context.helper(TO_DISPLAY_STRING) if (!context.ssr) {
context.helper(TO_DISPLAY_STRING)
}
break break
// for container types, further traverse downwards // for container types, further traverse downwards

View File

@ -17,7 +17,10 @@ import {
ForCodegenNode, ForCodegenNode,
ElementCodegenNode, ElementCodegenNode,
SlotOutletCodegenNode, SlotOutletCodegenNode,
SlotOutletNode SlotOutletNode,
ElementNode,
DirectiveNode,
ForNode
} from '../ast' } from '../ast'
import { createCompilerError, ErrorCodes } from '../errors' import { createCompilerError, ErrorCodes } from '../errors'
import { import {
@ -41,141 +44,163 @@ import { PatchFlags, PatchFlagNames } from '@vue/shared'
export const transformFor = createStructuralDirectiveTransform( export const transformFor = createStructuralDirectiveTransform(
'for', 'for',
(node, dir, context) => { (node, dir, context) => {
if (!dir.exp) { const { helper } = context
context.onError( return processForNode(node, dir, context, (forNode, parseResult) => {
createCompilerError(ErrorCodes.X_V_FOR_NO_EXPRESSION, dir.loc) // create the loop render function expression now, and add the
) // iterator on exit after all children have been traversed
return const renderExp = createCallExpression(helper(RENDER_LIST), [
} forNode.source
const parseResult = parseForExpression(
// can only be simple expression because vFor transform is applied
// before expression transform.
dir.exp as SimpleExpressionNode,
context
)
if (!parseResult) {
context.onError(
createCompilerError(ErrorCodes.X_V_FOR_MALFORMED_EXPRESSION, dir.loc)
)
return
}
const { helper, addIdentifiers, removeIdentifiers, scopes } = context
const { source, value, key, index } = parseResult
// create the loop render function expression now, and add the
// iterator on exit after all children have been traversed
const renderExp = createCallExpression(helper(RENDER_LIST), [source])
const keyProp = findProp(node, `key`)
const fragmentFlag = keyProp
? PatchFlags.KEYED_FRAGMENT
: PatchFlags.UNKEYED_FRAGMENT
const codegenNode = createSequenceExpression([
// fragment blocks disable tracking since they always diff their children
createCallExpression(helper(OPEN_BLOCK), [`false`]),
createCallExpression(helper(CREATE_BLOCK), [
helper(FRAGMENT),
`null`,
renderExp,
`${fragmentFlag} /* ${PatchFlagNames[fragmentFlag]} */`
]) ])
]) as ForCodegenNode const keyProp = findProp(node, `key`)
const fragmentFlag = keyProp
? PatchFlags.KEYED_FRAGMENT
: PatchFlags.UNKEYED_FRAGMENT
forNode.codegenNode = createSequenceExpression([
// fragment blocks disable tracking since they always diff their children
createCallExpression(helper(OPEN_BLOCK), [`false`]),
createCallExpression(helper(CREATE_BLOCK), [
helper(FRAGMENT),
`null`,
renderExp,
`${fragmentFlag} /* ${PatchFlagNames[fragmentFlag]} */`
])
]) as ForCodegenNode
context.replaceNode({ return () => {
type: NodeTypes.FOR, // finish the codegen now that all children have been traversed
loc: dir.loc, let childBlock
source, const isTemplate = isTemplateNode(node)
valueAlias: value, const slotOutlet = isSlotOutlet(node)
keyAlias: key, ? node
objectIndexAlias: index, : isTemplate &&
children: node.tagType === ElementTypes.TEMPLATE ? node.children : [node], node.children.length === 1 &&
codegenNode isSlotOutlet(node.children[0])
}) ? (node.children[0] as SlotOutletNode) // api-extractor somehow fails to infer this
: null
// bookkeeping const keyProperty = keyProp
scopes.vFor++ ? createObjectProperty(
if (!__BROWSER__ && context.prefixIdentifiers) { `key`,
// scope management keyProp.type === NodeTypes.ATTRIBUTE
// inject identifiers to context ? createSimpleExpression(keyProp.value!.content, true)
value && addIdentifiers(value) : keyProp.exp!
key && addIdentifiers(key) )
index && addIdentifiers(index)
}
return () => {
scopes.vFor--
if (!__BROWSER__ && context.prefixIdentifiers) {
value && removeIdentifiers(value)
key && removeIdentifiers(key)
index && removeIdentifiers(index)
}
// finish the codegen now that all children have been traversed
let childBlock
const isTemplate = isTemplateNode(node)
const slotOutlet = isSlotOutlet(node)
? node
: isTemplate &&
node.children.length === 1 &&
isSlotOutlet(node.children[0])
? (node.children[0] as SlotOutletNode) // api-extractor somehow fails to infer this
: null : null
const keyProperty = keyProp if (slotOutlet) {
? createObjectProperty( // <slot v-for="..."> or <template v-for="..."><slot/></template>
`key`, childBlock = slotOutlet.codegenNode as SlotOutletCodegenNode
keyProp.type === NodeTypes.ATTRIBUTE if (isTemplate && keyProperty) {
? createSimpleExpression(keyProp.value!.content, true) // <template v-for="..." :key="..."><slot/></template>
: keyProp.exp! // we need to inject the key to the renderSlot() call.
// the props for renderSlot is passed as the 3rd argument.
injectProp(childBlock, keyProperty, context)
}
} else if (isTemplate) {
// <template v-for="...">
// should generate a fragment block for each loop
childBlock = createBlockExpression(
createCallExpression(helper(CREATE_BLOCK), [
helper(FRAGMENT),
keyProperty ? createObjectExpression([keyProperty]) : `null`,
node.children,
`${PatchFlags.STABLE_FRAGMENT} /* ${
PatchFlagNames[PatchFlags.STABLE_FRAGMENT]
} */`
]),
context
) )
: null
if (slotOutlet) {
// <slot v-for="..."> or <template v-for="..."><slot/></template>
childBlock = slotOutlet.codegenNode as SlotOutletCodegenNode
if (isTemplate && keyProperty) {
// <template v-for="..." :key="..."><slot/></template>
// we need to inject the key to the renderSlot() call.
// the props for renderSlot is passed as the 3rd argument.
injectProp(childBlock, keyProperty, context)
}
} else if (isTemplate) {
// <template v-for="...">
// should generate a fragment block for each loop
childBlock = createBlockExpression(
createCallExpression(helper(CREATE_BLOCK), [
helper(FRAGMENT),
keyProperty ? createObjectExpression([keyProperty]) : `null`,
node.children,
`${PatchFlags.STABLE_FRAGMENT} /* ${
PatchFlagNames[PatchFlags.STABLE_FRAGMENT]
} */`
]),
context
)
} else {
// Normal element v-for. Directly use the child's codegenNode
// arguments, but replace createVNode() with createBlock()
let codegenNode = node.codegenNode as ElementCodegenNode
if (codegenNode.callee === WITH_DIRECTIVES) {
codegenNode.arguments[0].callee = helper(CREATE_BLOCK)
} else { } else {
codegenNode.callee = helper(CREATE_BLOCK) // Normal element v-for. Directly use the child's codegenNode
// arguments, but replace createVNode() with createBlock()
let codegenNode = node.codegenNode as ElementCodegenNode
if (codegenNode.callee === WITH_DIRECTIVES) {
codegenNode.arguments[0].callee = helper(CREATE_BLOCK)
} else {
codegenNode.callee = helper(CREATE_BLOCK)
}
childBlock = createBlockExpression(codegenNode, context)
} }
childBlock = createBlockExpression(codegenNode, context)
}
renderExp.arguments.push( renderExp.arguments.push(
createFunctionExpression( createFunctionExpression(
createForLoopParams(parseResult), createForLoopParams(parseResult),
childBlock, childBlock,
true /* force newline */ true /* force newline */
)
) )
) }
} })
} }
) )
// target-agnostic transform used for both Client and SSR
export function processForNode(
node: ElementNode,
dir: DirectiveNode,
context: TransformContext,
processCodegen?: (
forNode: ForNode,
parseResult: ForParseResult
) => (() => void) | undefined
) {
if (!dir.exp) {
context.onError(
createCompilerError(ErrorCodes.X_V_FOR_NO_EXPRESSION, dir.loc)
)
return
}
const parseResult = parseForExpression(
// can only be simple expression because vFor transform is applied
// before expression transform.
dir.exp as SimpleExpressionNode,
context
)
if (!parseResult) {
context.onError(
createCompilerError(ErrorCodes.X_V_FOR_MALFORMED_EXPRESSION, dir.loc)
)
return
}
const { addIdentifiers, removeIdentifiers, scopes } = context
const { source, value, key, index } = parseResult
const forNode: ForNode = {
type: NodeTypes.FOR,
loc: dir.loc,
source,
valueAlias: value,
keyAlias: key,
objectIndexAlias: index,
children: node.tagType === ElementTypes.TEMPLATE ? node.children : [node]
}
context.replaceNode(forNode)
// bookkeeping
scopes.vFor++
if (!__BROWSER__ && context.prefixIdentifiers) {
// scope management
// inject identifiers to context
value && addIdentifiers(value)
key && addIdentifiers(key)
index && addIdentifiers(index)
}
const onExit = processCodegen && processCodegen(forNode, parseResult)
return () => {
scopes.vFor--
if (!__BROWSER__ && context.prefixIdentifiers) {
value && removeIdentifiers(value)
key && removeIdentifiers(key)
index && removeIdentifiers(index)
}
if (onExit) onExit()
}
}
const forAliasRE = /([\s\S]*?)\s+(?:in|of)\s+([\s\S]*)/ const forAliasRE = /([\s\S]*?)\s+(?:in|of)\s+([\s\S]*)/
// This regex doesn't cover the case if key or index aliases have destructuring, // This regex doesn't cover the case if key or index aliases have destructuring,
// but those do not make sense in the first place, so this works in practice. // but those do not make sense in the first place, so this works in practice.

View File

@ -71,7 +71,8 @@ export const transformIf = createStructuralDirectiveTransform(
} }
) )
export const processIfBranches = ( // target-agnostic transform used for both Client and SSR
export function processIfBranches(
node: ElementNode, node: ElementNode,
dir: DirectiveNode, dir: DirectiveNode,
context: TransformContext, context: TransformContext,
@ -79,8 +80,8 @@ export const processIfBranches = (
node: IfNode, node: IfNode,
branch: IfBranchNode, branch: IfBranchNode,
isRoot: boolean isRoot: boolean
) => (() => void) | void ) => (() => void) | undefined
) => { ) {
if ( if (
dir.name !== 'else' && dir.name !== 'else' &&
(!dir.exp || !(dir.exp as SimpleExpressionNode).content.trim()) (!dir.exp || !(dir.exp as SimpleExpressionNode).content.trim())

View File

@ -34,13 +34,13 @@ describe('ssr: element', () => {
test('v-text', () => { test('v-text', () => {
expect(getCompiledString(`<div v-text="foo"/>`)).toMatchInlineSnapshot( expect(getCompiledString(`<div v-text="foo"/>`)).toMatchInlineSnapshot(
`"\`<div>\${interpolate(_ctx.foo)}</div>\`"` `"\`<div>\${_interpolate(_ctx.foo)}</div>\`"`
) )
}) })
test('<textarea> with dynamic value', () => { test('<textarea> with dynamic value', () => {
expect(getCompiledString(`<textarea :value="foo"/>`)).toMatchInlineSnapshot( expect(getCompiledString(`<textarea :value="foo"/>`)).toMatchInlineSnapshot(
`"\`<textarea>\${interpolate(_ctx.foo)}</textarea>\`"` `"\`<textarea>\${_interpolate(_ctx.foo)}</textarea>\`"`
) )
}) })

View File

@ -1,3 +1,4 @@
import { compile } from '../src'
import { getCompiledString } from './utils' import { getCompiledString } from './utils'
describe('ssr: text', () => { describe('ssr: text', () => {
@ -20,18 +21,25 @@ describe('ssr: text', () => {
}) })
test('interpolation', () => { test('interpolation', () => {
expect(getCompiledString(`foo {{ bar }} baz`)).toMatchInlineSnapshot( expect(compile(`foo {{ bar }} baz`).code).toMatchInlineSnapshot(`
`"\`foo \${interpolate(_ctx.bar)} baz\`"` "const { _interpolate } = require(\\"@vue/server-renderer\\")
)
return function ssrRender(_ctx, _push, _parent) {
_push(\`foo \${_interpolate(_ctx.bar)} baz\`)
}"
`)
}) })
test('nested elements with interpolation', () => { test('nested elements with interpolation', () => {
expect( expect(
getCompiledString( compile(`<div><span>{{ foo }} bar</span><span>baz {{ qux }}</span></div>`)
`<div><span>{{ foo }} bar</span><span>baz {{ qux }}</span></div>` .code
) ).toMatchInlineSnapshot(`
).toMatchInlineSnapshot( "const { _interpolate } = require(\\"@vue/server-renderer\\")
`"\`<div><span>\${interpolate(_ctx.foo)} bar</span><span>baz \${interpolate(_ctx.qux)}</span></div>\`"`
) return function ssrRender(_ctx, _push, _parent) {
_push(\`<div><span>\${_interpolate(_ctx.foo)} bar</span><span>baz \${_interpolate(_ctx.qux)}</span></div>\`)
}"
`)
}) })
}) })

View File

@ -22,20 +22,23 @@ export function compile(
template: string, template: string,
options: SSRCompilerOptions = {} options: SSRCompilerOptions = {}
): CodegenResult { ): CodegenResult {
// apply DOM-specific parsing options
options = { options = {
mode: 'cjs',
...options,
// apply DOM-specific parsing options
...parserOptions, ...parserOptions,
...options ssr: true,
// always prefix since compiler-ssr doesn't have size concern
prefixIdentifiers: true,
// disalbe optimizations that are unnecessary for ssr
cacheHandlers: false,
hoistStatic: false
} }
const ast = baseParse(template, options) const ast = baseParse(template, options)
transform(ast, { transform(ast, {
...options, ...options,
prefixIdentifiers: true,
// disalbe optimizations that are unnecessary for ssr
cacheHandlers: false,
hoistStatic: false,
nodeTransforms: [ nodeTransforms: [
ssrTransformIf, ssrTransformIf,
ssrTransformFor, ssrTransformFor,
@ -57,10 +60,5 @@ export function compile(
// by replacing ast.codegenNode. // by replacing ast.codegenNode.
ssrCodegenTransform(ast, options) ssrCodegenTransform(ast, options)
return generate(ast, { return generate(ast, options)
mode: 'cjs',
...options,
ssr: true,
prefixIdentifiers: true
})
} }

View File

@ -1,7 +1,21 @@
import { registerRuntimeHelpers } from '@vue/compiler-dom' import { registerRuntimeHelpers } from '@vue/compiler-dom'
export const INTERPOLATE = Symbol(`interpolate`) export const SSR_INTERPOLATE = Symbol(`interpolate`)
export const SSR_RENDER_COMPONENT = Symbol(`renderComponent`)
export const SSR_RENDER_SLOT = Symbol(`renderSlot`)
export const SSR_RENDER_CLASS = Symbol(`renderClass`)
export const SSR_RENDER_STYLE = Symbol(`renderStyle`)
export const SSR_RENDER_PROPS = Symbol(`renderProps`)
export const SSR_RENDER_LIST = Symbol(`renderList`)
// Note: these are helpers imported from @vue/server-renderer
// make sure the names match!
registerRuntimeHelpers({ registerRuntimeHelpers({
[INTERPOLATE]: `interpolate` [SSR_INTERPOLATE]: `_interpolate`,
[SSR_RENDER_COMPONENT]: `_renderComponent`,
[SSR_RENDER_SLOT]: `_renderSlot`,
[SSR_RENDER_CLASS]: `_renderClass`,
[SSR_RENDER_STYLE]: `_renderStyle`,
[SSR_RENDER_PROPS]: `_renderProps`,
[SSR_RENDER_LIST]: `_renderList`
}) })

View File

@ -14,8 +14,9 @@ import {
CallExpression CallExpression
} from '@vue/compiler-dom' } from '@vue/compiler-dom'
import { isString, escapeHtml, NO } from '@vue/shared' import { isString, escapeHtml, NO } from '@vue/shared'
import { INTERPOLATE } from './runtimeHelpers' import { SSR_INTERPOLATE } from './runtimeHelpers'
import { processIf } from './transforms/ssrVIf' import { processIf } from './transforms/ssrVIf'
import { processFor } from './transforms/ssrVFor'
// Because SSR codegen output is completely different from client-side output // Because SSR codegen output is completely different from client-side output
// (e.g. multiple elements can be concatenated into a single template literal // (e.g. multiple elements can be concatenated into a single template literal
@ -37,17 +38,26 @@ export function ssrCodegenTransform(ast: RootNode, options: CompilerOptions) {
} }
ast.codegenNode = createBlockStatement(context.body) ast.codegenNode = createBlockStatement(context.body)
ast.ssrHelpers = [...context.helpers]
} }
export type SSRTransformContext = ReturnType<typeof createSSRTransformContext> export type SSRTransformContext = ReturnType<typeof createSSRTransformContext>
export function createSSRTransformContext(options: CompilerOptions) { function createSSRTransformContext(
options: CompilerOptions,
helpers: Set<symbol> = new Set()
) {
const body: BlockStatement['body'] = [] const body: BlockStatement['body'] = []
let currentString: TemplateLiteral | null = null let currentString: TemplateLiteral | null = null
return { return {
options, options,
body, body,
helpers,
helper<T extends symbol>(name: T): T {
helpers.add(name)
return name
},
pushStringPart(part: TemplateLiteral['elements'][0]) { pushStringPart(part: TemplateLiteral['elements'][0]) {
if (!currentString) { if (!currentString) {
const currentCall = createCallExpression(`_push`) const currentCall = createCallExpression(`_push`)
@ -71,6 +81,13 @@ export function createSSRTransformContext(options: CompilerOptions) {
} }
} }
export function createChildContext(
parent: SSRTransformContext
): SSRTransformContext {
// ensure child inherits parent helpers
return createSSRTransformContext(parent.options, parent.helpers)
}
export function processChildren( export function processChildren(
children: TemplateChildNode[], children: TemplateChildNode[],
context: SSRTransformContext context: SSRTransformContext
@ -100,11 +117,13 @@ export function processChildren(
} else if (child.type === NodeTypes.TEXT) { } else if (child.type === NodeTypes.TEXT) {
context.pushStringPart(escapeHtml(child.content)) context.pushStringPart(escapeHtml(child.content))
} else if (child.type === NodeTypes.INTERPOLATION) { } else if (child.type === NodeTypes.INTERPOLATION) {
context.pushStringPart(createCallExpression(INTERPOLATE, [child.content])) context.pushStringPart(
createCallExpression(context.helper(SSR_INTERPOLATE), [child.content])
)
} else if (child.type === NodeTypes.IF) { } else if (child.type === NodeTypes.IF) {
processIf(child, context) processIf(child, context)
} else if (child.type === NodeTypes.FOR) { } else if (child.type === NodeTypes.FOR) {
// TODO processFor(child, context)
} }
} }
} }

View File

@ -1,3 +1,16 @@
import { NodeTransform } from '@vue/compiler-dom' import {
createStructuralDirectiveTransform,
ForNode,
processForNode
} from '@vue/compiler-dom'
import { SSRTransformContext } from '../ssrCodegenTransform'
export const ssrTransformFor: NodeTransform = () => {} // Plugin for the first transform pass, which simply constructs the AST node
export const ssrTransformFor = createStructuralDirectiveTransform(
'for',
processForNode
)
// This is called during the 2nd transform pass to construct the SSR-sepcific
// codegen nodes.
export function processFor(node: ForNode, context: SSRTransformContext) {}

View File

@ -11,12 +11,11 @@ import {
} from '@vue/compiler-dom' } from '@vue/compiler-dom'
import { import {
SSRTransformContext, SSRTransformContext,
createSSRTransformContext, createChildContext,
processChildren processChildren
} from '../ssrCodegenTransform' } from '../ssrCodegenTransform'
// This is the plugin for the first transform pass, which simply constructs the // Plugin for the first transform pass, which simply constructs the AST node
// if node and its branches.
export const ssrTransformIf = createStructuralDirectiveTransform( export const ssrTransformIf = createStructuralDirectiveTransform(
/^(if|else|else-if)$/, /^(if|else|else-if)$/,
processIfBranches processIfBranches
@ -64,7 +63,7 @@ function processIfBranch(
// TODO optimize away nested fragments when the only child is a ForNode // TODO optimize away nested fragments when the only child is a ForNode
const needFragmentWrapper = const needFragmentWrapper =
children.length !== 1 || firstChild.type !== NodeTypes.ELEMENT children.length !== 1 || firstChild.type !== NodeTypes.ELEMENT
const childContext = createSSRTransformContext(context.options) const childContext = createChildContext(context)
if (needFragmentWrapper) { if (needFragmentWrapper) {
childContext.pushStringPart(`<!---->`) childContext.pushStringPart(`<!---->`)
} }

View File

@ -1,15 +1,16 @@
import { escapeHtml, interpolate } from '../src' import { _interpolate } from '../src'
import { escapeHtml } from '@vue/shared'
test('ssr: interpolate', () => { test('ssr: interpolate', () => {
expect(interpolate(0)).toBe(`0`) expect(_interpolate(0)).toBe(`0`)
expect(interpolate(`foo`)).toBe(`foo`) expect(_interpolate(`foo`)).toBe(`foo`)
expect(interpolate(`<div>`)).toBe(`&lt;div&gt;`) expect(_interpolate(`<div>`)).toBe(`&lt;div&gt;`)
// should escape interpolated values // should escape interpolated values
expect(interpolate([1, 2, 3])).toBe( expect(_interpolate([1, 2, 3])).toBe(
escapeHtml(JSON.stringify([1, 2, 3], null, 2)) escapeHtml(JSON.stringify([1, 2, 3], null, 2))
) )
expect( expect(
interpolate({ _interpolate({
foo: 1, foo: 1,
bar: `<div>` bar: `<div>`
}) })

View File

@ -1,4 +1,4 @@
import { renderProps, renderClass, renderStyle } from '../src' import { renderProps, renderClass, renderStyle } from '../src/renderProps'
describe('ssr: renderProps', () => { describe('ssr: renderProps', () => {
test('ignore reserved props', () => { test('ignore reserved props', () => {

View File

@ -6,7 +6,12 @@ import {
resolveComponent, resolveComponent,
ComponentOptions ComponentOptions
} from 'vue' } from 'vue'
import { renderToString, renderComponent, renderSlot, escapeHtml } from '../src' import { escapeHtml } from '@vue/shared'
import {
renderToString,
renderComponent,
renderSlot
} from '../src/renderToString'
describe('ssr: renderToString', () => { describe('ssr: renderToString', () => {
test('should apply app context', async () => { test('should apply app context', async () => {

View File

@ -2,15 +2,19 @@
export { renderToString } from './renderToString' export { renderToString } from './renderToString'
// internal // internal
export { renderComponent, renderSlot } from './renderToString' export {
export { renderClass, renderStyle, renderProps } from './renderProps' renderComponent as _renderComponent,
renderSlot as _renderSlot
} from './renderToString'
export {
renderClass as _renderClass,
renderStyle as _renderStyle,
renderProps as _renderProps
} from './renderProps'
// utils // utils
import { escapeHtml as _escapeHtml, toDisplayString } from '@vue/shared' import { escapeHtml, toDisplayString } from '@vue/shared'
// cast type to avoid dts dependency on @vue/shared (which is inlined) export function _interpolate(value: unknown): string {
export const escapeHtml = _escapeHtml as (raw: string) => string
export function interpolate(value: unknown): string {
return escapeHtml(toDisplayString(value)) return escapeHtml(toDisplayString(value))
} }