diff --git a/packages/compiler-core/src/ast.ts b/packages/compiler-core/src/ast.ts index d686c1aa..ac8f89c4 100644 --- a/packages/compiler-core/src/ast.ts +++ b/packages/compiler-core/src/ast.ts @@ -103,6 +103,7 @@ export interface RootNode extends Node { imports: ImportItem[] cached: number codegenNode?: TemplateChildNode | JSChildNode | BlockStatement | undefined + ssrHelpers?: symbol[] } export type ElementNode = diff --git a/packages/compiler-core/src/codegen.ts b/packages/compiler-core/src/codegen.ts index 245e33b0..d558ec65 100644 --- a/packages/compiler-core/src/codegen.ts +++ b/packages/compiler-core/src/codegen.ts @@ -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', diff --git a/packages/compiler-core/src/index.ts b/packages/compiler-core/src/index.ts index b87cfe42..6b63067d 100644 --- a/packages/compiler-core/src/index.ts +++ b/packages/compiler-core/src/index.ts @@ -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 diff --git a/packages/compiler-core/src/options.ts b/packages/compiler-core/src/options.ts index 7a0c866c..f6dde0fa 100644 --- a/packages/compiler-core/src/options.ts +++ b/packages/compiler-core/src/options.ts @@ -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 } diff --git a/packages/compiler-core/src/transform.ts b/packages/compiler-core/src/transform.ts index 7029c085..35c81e8b 100644 --- a/packages/compiler-core/src/transform.ts +++ b/packages/compiler-core/src/transform.ts @@ -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 diff --git a/packages/compiler-core/src/transforms/vFor.ts b/packages/compiler-core/src/transforms/vFor.ts index a31a47e8..077101ab 100644 --- a/packages/compiler-core/src/transforms/vFor.ts +++ b/packages/compiler-core/src/transforms/vFor.ts @@ -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) { + // or + childBlock = slotOutlet.codegenNode as SlotOutletCodegenNode + if (isTemplate && keyProperty) { + // + // 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) { + //