wip(ssr): ssr helper codegen
This commit is contained in:
parent
d1d81cf1f9
commit
b685805a26
@ -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 =
|
||||||
|
@ -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 {
|
} else {
|
||||||
// "with" mode.
|
genFunctionPreamble(ast, context)
|
||||||
// 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 {
|
|
||||||
// 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 `)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 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',
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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:
|
||||||
|
if (!context.ssr) {
|
||||||
// inject import for the Comment symbol, which is needed for creating
|
// inject import for the Comment symbol, which is needed for creating
|
||||||
// comment nodes with `createVNode`
|
// comment nodes with `createVNode`
|
||||||
context.helper(CREATE_COMMENT)
|
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
|
||||||
|
if (!context.ssr) {
|
||||||
context.helper(TO_DISPLAY_STRING)
|
context.helper(TO_DISPLAY_STRING)
|
||||||
|
}
|
||||||
break
|
break
|
||||||
|
|
||||||
// for container types, further traverse downwards
|
// for container types, further traverse downwards
|
||||||
|
@ -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,38 +44,18 @@ 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)
|
|
||||||
)
|
|
||||||
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
|
// create the loop render function expression now, and add the
|
||||||
// iterator on exit after all children have been traversed
|
// iterator on exit after all children have been traversed
|
||||||
const renderExp = createCallExpression(helper(RENDER_LIST), [source])
|
const renderExp = createCallExpression(helper(RENDER_LIST), [
|
||||||
|
forNode.source
|
||||||
|
])
|
||||||
const keyProp = findProp(node, `key`)
|
const keyProp = findProp(node, `key`)
|
||||||
const fragmentFlag = keyProp
|
const fragmentFlag = keyProp
|
||||||
? PatchFlags.KEYED_FRAGMENT
|
? PatchFlags.KEYED_FRAGMENT
|
||||||
: PatchFlags.UNKEYED_FRAGMENT
|
: PatchFlags.UNKEYED_FRAGMENT
|
||||||
const codegenNode = createSequenceExpression([
|
forNode.codegenNode = createSequenceExpression([
|
||||||
// fragment blocks disable tracking since they always diff their children
|
// fragment blocks disable tracking since they always diff their children
|
||||||
createCallExpression(helper(OPEN_BLOCK), [`false`]),
|
createCallExpression(helper(OPEN_BLOCK), [`false`]),
|
||||||
createCallExpression(helper(CREATE_BLOCK), [
|
createCallExpression(helper(CREATE_BLOCK), [
|
||||||
@ -83,35 +66,7 @@ export const transformFor = createStructuralDirectiveTransform(
|
|||||||
])
|
])
|
||||||
]) as ForCodegenNode
|
]) 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 () => {
|
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
|
// finish the codegen now that all children have been traversed
|
||||||
let childBlock
|
let childBlock
|
||||||
const isTemplate = isTemplateNode(node)
|
const isTemplate = isTemplateNode(node)
|
||||||
@ -173,9 +128,79 @@ export const transformFor = createStructuralDirectiveTransform(
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// 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.
|
||||||
|
@ -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())
|
||||||
|
@ -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>\`"`
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -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>\`)
|
||||||
|
}"
|
||||||
|
`)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
@ -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
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
@ -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`
|
||||||
})
|
})
|
||||||
|
@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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) {}
|
||||||
|
@ -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(`<!---->`)
|
||||||
}
|
}
|
||||||
|
@ -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(`<div>`)
|
expect(_interpolate(`<div>`)).toBe(`<div>`)
|
||||||
// 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>`
|
||||||
})
|
})
|
||||||
|
@ -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', () => {
|
||||||
|
@ -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 () => {
|
||||||
|
@ -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))
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user