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[] | ||||
|   cached: number | ||||
|   codegenNode?: TemplateChildNode | JSChildNode | BlockStatement | undefined | ||||
|   ssrHelpers?: symbol[] | ||||
| } | ||||
| 
 | ||||
| export type ElementNode = | ||||
|  | ||||
| @ -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', | ||||
|  | ||||
| @ -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 | ||||
|  | ||||
| @ -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 | ||||
| } | ||||
| 
 | ||||
|  | ||||
| @ -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
 | ||||
|  | ||||
| @ -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.
 | ||||
|  | ||||
| @ -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()) | ||||
|  | ||||
| @ -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>\`"` | ||||
|     ) | ||||
|   }) | ||||
| 
 | ||||
|  | ||||
| @ -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>\`)
 | ||||
|       }" | ||||
|     `)
 | ||||
|   }) | ||||
| }) | ||||
|  | ||||
| @ -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) | ||||
| } | ||||
|  | ||||
| @ -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` | ||||
| }) | ||||
|  | ||||
| @ -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) | ||||
|     } | ||||
|   } | ||||
| } | ||||
|  | ||||
| @ -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' | ||||
| 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(`<!---->`) | ||||
|   } | ||||
|  | ||||
| @ -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(`<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>` | ||||
|     }) | ||||
|  | ||||
| @ -1,4 +1,4 @@ | ||||
| import { renderProps, renderClass, renderStyle } from '../src' | ||||
| import { renderProps, renderClass, renderStyle } from '../src/renderProps' | ||||
| 
 | ||||
| describe('ssr: renderProps', () => { | ||||
|   test('ignore reserved props', () => { | ||||
|  | ||||
| @ -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 () => { | ||||
|  | ||||
| @ -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)) | ||||
| } | ||||
|  | ||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user