imports: ImportItem[]
cached: number
codegenNode?: TemplateChildNode | JSChildNode | BlockStatement | undefined
+ ssrHelpers?: symbol[]
}
export type ElementNode =
ast: RootNode,
{
mode = 'function',
- prefixIdentifiers = mode === 'module' || mode === 'cjs',
+ prefixIdentifiers = mode === 'module',
sourceMap = false,
filename = `template.vue.html`,
scopeId = null,
const {
mode,
push,
- helper,
prefixIdentifiers,
indent,
deindent,
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
}
}
+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',
// exported for compiler-ssr
export { processIfBranches } from './transforms/vIf'
+export { processForNode } from './transforms/vFor'
export {
transformExpression,
processExpression
// 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
}
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'
// analysis to determine if a handler is safe to cache.
// - Default: false
cacheHandlers?: boolean
+ ssr?: boolean
onError?: (error: CompilerError) => void
}
// `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
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
}
nodeTransforms = [],
directiveTransforms = {},
isBuiltInComponent = NOOP,
+ ssr = false,
onError = defaultOnError
}: TransformOptions
): TransformContext {
nodeTransforms,
directiveTransforms,
isBuiltInComponent,
+ ssr,
onError,
// state
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]
} 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(
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
ForCodegenNode,
ElementCodegenNode,
SlotOutletCodegenNode,
- SlotOutletNode
+ SlotOutletNode,
+ ElementNode,
+ DirectiveNode,
+ ForNode
} from '../ast'
import { createCompilerError, ErrorCodes } from '../errors'
import {
export const transformFor = createStructuralDirectiveTransform(
'for',
(node, dir, context) => {
- if (!dir.exp) {
- context.onError(
- createCompilerError(ErrorCodes.X_V_FOR_NO_EXPRESSION, dir.loc)
- )
- return
- }
+ 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
+ ])
+ 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
- const parseResult = parseForExpression(
- // can only be simple expression because vFor transform is applied
- // before expression transform.
- dir.exp as SimpleExpressionNode,
- context
+ 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
+ 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)
+ }
+ childBlock = createBlockExpression(codegenNode, context)
+ }
+
+ 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
+ }
- if (!parseResult) {
- context.onError(
- createCompilerError(ErrorCodes.X_V_FOR_MALFORMED_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
+ )
- const { helper, addIdentifiers, removeIdentifiers, scopes } = context
- const { source, value, key, index } = parseResult
+ if (!parseResult) {
+ context.onError(
+ createCompilerError(ErrorCodes.X_V_FOR_MALFORMED_EXPRESSION, dir.loc)
+ )
+ return
+ }
- // 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 { addIdentifiers, removeIdentifiers, scopes } = context
+ const { source, value, key, index } = parseResult
- context.replaceNode({
- type: NodeTypes.FOR,
- loc: dir.loc,
- source,
- valueAlias: value,
- keyAlias: key,
- objectIndexAlias: index,
- children: node.tagType === ElementTypes.TEMPLATE ? node.children : [node],
- codegenNode
- })
+ const forNode: ForNode = {
+ type: NodeTypes.FOR,
+ loc: dir.loc,
+ source,
+ valueAlias: value,
+ keyAlias: key,
+ objectIndexAlias: index,
+ children: node.tagType === ElementTypes.TEMPLATE ? node.children : [node]
+ }
- // bookkeeping
- scopes.vFor++
- if (!__BROWSER__ && context.prefixIdentifiers) {
- // scope management
- // inject identifiers to context
- value && addIdentifiers(value)
- key && addIdentifiers(key)
- index && addIdentifiers(index)
- }
+ context.replaceNode(forNode)
- return () => {
- scopes.vFor--
- if (!__BROWSER__ && context.prefixIdentifiers) {
- value && removeIdentifiers(value)
- key && removeIdentifiers(key)
- index && removeIdentifiers(index)
- }
+ // bookkeeping
+ scopes.vFor++
+ if (!__BROWSER__ && context.prefixIdentifiers) {
+ // scope management
+ // inject identifiers to context
+ value && addIdentifiers(value)
+ key && addIdentifiers(key)
+ index && addIdentifiers(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
- const keyProperty = keyProp
- ? createObjectProperty(
- `key`,
- keyProp.type === NodeTypes.ATTRIBUTE
- ? createSimpleExpression(keyProp.value!.content, true)
- : keyProp.exp!
- )
- : 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)
- }
- childBlock = createBlockExpression(codegenNode, context)
- }
+ const onExit = processCodegen && processCodegen(forNode, parseResult)
- renderExp.arguments.push(
- createFunctionExpression(
- createForLoopParams(parseResult),
- childBlock,
- true /* force newline */
- )
- )
+ 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,
}
)
-export const processIfBranches = (
+// target-agnostic transform used for both Client and SSR
+export function processIfBranches(
node: ElementNode,
dir: DirectiveNode,
context: TransformContext,
node: IfNode,
branch: IfBranchNode,
isRoot: boolean
- ) => (() => void) | void
-) => {
+ ) => (() => void) | undefined
+) {
if (
dir.name !== 'else' &&
(!dir.exp || !(dir.exp as SimpleExpressionNode).content.trim())
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>\`"`
)
})
+import { compile } from '../src'
import { getCompiledString } from './utils'
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>\`)
+ }"
+ `)
})
})
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,
// by replacing ast.codegenNode.
ssrCodegenTransform(ast, options)
- return generate(ast, {
- mode: 'cjs',
- ...options,
- ssr: true,
- prefixIdentifiers: true
- })
+ return generate(ast, options)
}
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`
})
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
}
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`)
}
}
+export function createChildContext(
+ parent: SSRTransformContext
+): SSRTransformContext {
+ // ensure child inherits parent helpers
+ return createSSRTransformContext(parent.options, parent.helpers)
+}
+
export function processChildren(
children: TemplateChildNode[],
context: SSRTransformContext
} 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)
}
}
}
-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) {}
} 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
// 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(`<!---->`)
}
-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(`<div>`)
+ expect(_interpolate(0)).toBe(`0`)
+ expect(_interpolate(`foo`)).toBe(`foo`)
+ expect(_interpolate(`<div>`)).toBe(`<div>`)
// 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>`
})
-import { renderProps, renderClass, renderStyle } from '../src'
+import { renderProps, renderClass, renderStyle } from '../src/renderProps'
describe('ssr: renderProps', () => {
test('ignore reserved props', () => {
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 () => {
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))
}