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[]
cached: number
codegenNode?: TemplateChildNode | JSChildNode | BlockStatement | undefined
ssrHelpers?: symbol[]
}
export type ElementNode =

View File

@ -74,7 +74,7 @@ function createCodegenContext(
ast: RootNode,
{
mode = 'function',
prefixIdentifiers = mode === 'module' || mode === 'cjs',
prefixIdentifiers = mode === 'module',
sourceMap = false,
filename = `template.vue.html`,
scopeId = null,
@ -169,7 +169,6 @@ export function generate(
const {
mode,
push,
helper,
prefixIdentifiers,
indent,
deindent,
@ -182,58 +181,10 @@ export function generate(
const genScopeId = !__BROWSER__ && scopeId != null && mode === 'module'
// preambles
if (mode === 'function' || mode === 'cjs') {
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 (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 `)
if (mode === 'module') {
genModulePreamble(ast, context, genScopeId)
} else {
// 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 (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 `)
genFunctionPreamble(ast, context)
}
// 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(
assets: string[],
type: 'component' | 'directive',

View File

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

View File

@ -23,7 +23,6 @@ export interface ParserOptions {
// this number is based on the map above, but it should be pre-computed
// to avoid the cost on every parse() call.
maxCRNameLength?: number
onError?: (error: CompilerError) => void
}
@ -32,6 +31,8 @@ export interface TransformOptions {
directiveTransforms?: { [name: string]: DirectiveTransform | undefined }
isBuiltInComponent?: (tag: string) => symbol | void
// 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
// and cannot use `with`
// - Default: mode === 'module'
@ -48,6 +49,7 @@ export interface TransformOptions {
// analysis to determine if a handler is safe to cache.
// - Default: false
cacheHandlers?: boolean
ssr?: boolean
onError?: (error: CompilerError) => void
}
@ -61,13 +63,6 @@ export interface CodegenOptions {
// `require('vue')`.
// - Default: 'function'
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?
// - Default: false
sourceMap?: boolean
@ -76,7 +71,9 @@ export interface CodegenOptions {
filename?: string
// SFC scoped styles ID
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
}

View File

@ -115,6 +115,7 @@ function createTransformContext(
nodeTransforms = [],
directiveTransforms = {},
isBuiltInComponent = NOOP,
ssr = false,
onError = defaultOnError
}: TransformOptions
): TransformContext {
@ -126,6 +127,7 @@ function createTransformContext(
nodeTransforms,
directiveTransforms,
isBuiltInComponent,
ssr,
onError,
// state
@ -256,10 +258,19 @@ export function transform(root: RootNode, options: TransformOptions) {
if (options.hoistStatic) {
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 { children } = root
const child = children[0]
@ -304,13 +315,6 @@ function finalizeRoot(root: RootNode, context: TransformContext) {
} else {
// 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(
@ -359,13 +363,17 @@ export function traverseNode(
switch (node.type) {
case NodeTypes.COMMENT:
// inject import for the Comment symbol, which is needed for creating
// comment nodes with `createVNode`
context.helper(CREATE_COMMENT)
if (!context.ssr) {
// inject import for the Comment symbol, which is needed for creating
// comment nodes with `createVNode`
context.helper(CREATE_COMMENT)
}
break
case NodeTypes.INTERPOLATION:
// 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
// for container types, further traverse downwards

View File

@ -17,7 +17,10 @@ import {
ForCodegenNode,
ElementCodegenNode,
SlotOutletCodegenNode,
SlotOutletNode
SlotOutletNode,
ElementNode,
DirectiveNode,
ForNode
} from '../ast'
import { createCompilerError, ErrorCodes } from '../errors'
import {
@ -41,141 +44,163 @@ import { PatchFlags, PatchFlagNames } from '@vue/shared'
export const transformFor = createStructuralDirectiveTransform(
'for',
(node, dir, context) => {
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 { 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]} */`
const { helper } = context
return processForNode(node, dir, context, (forNode, 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), [
forNode.source
])
]) 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({
type: NodeTypes.FOR,
loc: dir.loc,
source,
valueAlias: value,
keyAlias: key,
objectIndexAlias: index,
children: node.tagType === ElementTypes.TEMPLATE ? node.children : [node],
codegenNode
})
// bookkeeping
scopes.vFor++
if (!__BROWSER__ && context.prefixIdentifiers) {
// scope management
// inject identifiers to context
value && addIdentifiers(value)
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
return () => {
// 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
const keyProperty = keyProp
? createObjectProperty(
`key`,
keyProp.type === NodeTypes.ATTRIBUTE
? createSimpleExpression(keyProp.value!.content, true)
: keyProp.exp!
)
: null
const keyProperty = keyProp
? createObjectProperty(
`key`,
keyProp.type === NodeTypes.ATTRIBUTE
? createSimpleExpression(keyProp.value!.content, true)
: keyProp.exp!
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
)
: 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 {
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(
createFunctionExpression(
createForLoopParams(parseResult),
childBlock,
true /* force newline */
renderExp.arguments.push(
createFunctionExpression(
createForLoopParams(parseResult),
childBlock,
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]*)/
// 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.

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,
dir: DirectiveNode,
context: TransformContext,
@ -79,8 +80,8 @@ export const processIfBranches = (
node: IfNode,
branch: IfBranchNode,
isRoot: boolean
) => (() => void) | void
) => {
) => (() => void) | undefined
) {
if (
dir.name !== 'else' &&
(!dir.exp || !(dir.exp as SimpleExpressionNode).content.trim())

View File

@ -34,13 +34,13 @@ describe('ssr: element', () => {
test('v-text', () => {
expect(getCompiledString(`<div v-text="foo"/>`)).toMatchInlineSnapshot(
`"\`<div>\${interpolate(_ctx.foo)}</div>\`"`
`"\`<div>\${_interpolate(_ctx.foo)}</div>\`"`
)
})
test('<textarea> with dynamic value', () => {
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'
describe('ssr: text', () => {
@ -20,18 +21,25 @@ describe('ssr: text', () => {
})
test('interpolation', () => {
expect(getCompiledString(`foo {{ bar }} baz`)).toMatchInlineSnapshot(
`"\`foo \${interpolate(_ctx.bar)} baz\`"`
)
expect(compile(`foo {{ bar }} baz`).code).toMatchInlineSnapshot(`
"const { _interpolate } = require(\\"@vue/server-renderer\\")
return function ssrRender(_ctx, _push, _parent) {
_push(\`foo \${_interpolate(_ctx.bar)} baz\`)
}"
`)
})
test('nested elements with interpolation', () => {
expect(
getCompiledString(
`<div><span>{{ foo }} bar</span><span>baz {{ qux }}</span></div>`
)
).toMatchInlineSnapshot(
`"\`<div><span>\${interpolate(_ctx.foo)} bar</span><span>baz \${interpolate(_ctx.qux)}</span></div>\`"`
)
compile(`<div><span>{{ foo }} bar</span><span>baz {{ qux }}</span></div>`)
.code
).toMatchInlineSnapshot(`
"const { _interpolate } = require(\\"@vue/server-renderer\\")
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,
options: SSRCompilerOptions = {}
): CodegenResult {
// apply DOM-specific parsing options
options = {
mode: 'cjs',
...options,
// apply DOM-specific parsing options
...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)
transform(ast, {
...options,
prefixIdentifiers: true,
// disalbe optimizations that are unnecessary for ssr
cacheHandlers: false,
hoistStatic: false,
nodeTransforms: [
ssrTransformIf,
ssrTransformFor,
@ -57,10 +60,5 @@ export function compile(
// by replacing ast.codegenNode.
ssrCodegenTransform(ast, options)
return generate(ast, {
mode: 'cjs',
...options,
ssr: true,
prefixIdentifiers: true
})
return generate(ast, options)
}

View File

@ -1,7 +1,21 @@
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({
[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
} from '@vue/compiler-dom'
import { isString, escapeHtml, NO } from '@vue/shared'
import { INTERPOLATE } from './runtimeHelpers'
import { SSR_INTERPOLATE } from './runtimeHelpers'
import { processIf } from './transforms/ssrVIf'
import { processFor } from './transforms/ssrVFor'
// Because SSR codegen output is completely different from client-side output
// (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.ssrHelpers = [...context.helpers]
}
export type SSRTransformContext = ReturnType<typeof createSSRTransformContext>
export function createSSRTransformContext(options: CompilerOptions) {
function createSSRTransformContext(
options: CompilerOptions,
helpers: Set<symbol> = new Set()
) {
const body: BlockStatement['body'] = []
let currentString: TemplateLiteral | null = null
return {
options,
body,
helpers,
helper<T extends symbol>(name: T): T {
helpers.add(name)
return name
},
pushStringPart(part: TemplateLiteral['elements'][0]) {
if (!currentString) {
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(
children: TemplateChildNode[],
context: SSRTransformContext
@ -100,11 +117,13 @@ export function processChildren(
} else if (child.type === NodeTypes.TEXT) {
context.pushStringPart(escapeHtml(child.content))
} 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) {
processIf(child, context)
} 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'
import {
SSRTransformContext,
createSSRTransformContext,
createChildContext,
processChildren
} from '../ssrCodegenTransform'
// This is the plugin for the first transform pass, which simply constructs the
// if node and its branches.
// Plugin for the first transform pass, which simply constructs the AST node
export const ssrTransformIf = createStructuralDirectiveTransform(
/^(if|else|else-if)$/,
processIfBranches
@ -64,7 +63,7 @@ function processIfBranch(
// TODO optimize away nested fragments when the only child is a ForNode
const needFragmentWrapper =
children.length !== 1 || firstChild.type !== NodeTypes.ELEMENT
const childContext = createSSRTransformContext(context.options)
const childContext = createChildContext(context)
if (needFragmentWrapper) {
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', () => {
expect(interpolate(0)).toBe(`0`)
expect(interpolate(`foo`)).toBe(`foo`)
expect(interpolate(`<div>`)).toBe(`&lt;div&gt;`)
expect(_interpolate(0)).toBe(`0`)
expect(_interpolate(`foo`)).toBe(`foo`)
expect(_interpolate(`<div>`)).toBe(`&lt;div&gt;`)
// should escape interpolated values
expect(interpolate([1, 2, 3])).toBe(
expect(_interpolate([1, 2, 3])).toBe(
escapeHtml(JSON.stringify([1, 2, 3], null, 2))
)
expect(
interpolate({
_interpolate({
foo: 1,
bar: `<div>`
})

View File

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

View File

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

View File

@ -2,15 +2,19 @@
export { renderToString } from './renderToString'
// internal
export { renderComponent, renderSlot } from './renderToString'
export { renderClass, renderStyle, renderProps } from './renderProps'
export {
renderComponent as _renderComponent,
renderSlot as _renderSlot
} from './renderToString'
export {
renderClass as _renderClass,
renderStyle as _renderStyle,
renderProps as _renderProps
} from './renderProps'
// 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 const escapeHtml = _escapeHtml as (raw: string) => string
export function interpolate(value: unknown): string {
export function _interpolate(value: unknown): string {
return escapeHtml(toDisplayString(value))
}