diff --git a/packages/compiler-core/__tests__/transforms/transformElement.spec.ts b/packages/compiler-core/__tests__/transforms/transformElement.spec.ts index 138fa1005c4..e7a95622d1a 100644 --- a/packages/compiler-core/__tests__/transforms/transformElement.spec.ts +++ b/packages/compiler-core/__tests__/transforms/transformElement.spec.ts @@ -5,7 +5,8 @@ import { ErrorCodes, BindingTypes, NodeTransform, - transformExpression + transformExpression, + baseCompile } from '../../src' import { RESOLVE_COMPONENT, @@ -66,6 +67,7 @@ function parseWithBind(template: string, options?: CompilerOptions) { return parseWithElementTransform(template, { ...options, directiveTransforms: { + ...options?.directiveTransforms, bind: transformBind } }) @@ -932,7 +934,11 @@ describe('compiler: element transform', () => { }) test('NEED_PATCH (vnode hooks)', () => { - const { node } = parseWithBind(`
`) + const root = baseCompile(`
`, { + prefixIdentifiers: true, + cacheHandlers: true + }).ast + const node = (root as any).children[0].codegenNode expect(node.patchFlag).toBe(genFlagText(PatchFlags.NEED_PATCH)) }) diff --git a/packages/compiler-core/src/index.ts b/packages/compiler-core/src/index.ts index 1233e0ada59..a68d2395815 100644 --- a/packages/compiler-core/src/index.ts +++ b/packages/compiler-core/src/index.ts @@ -54,7 +54,9 @@ export { export { transformElement, resolveComponentType, - buildProps + buildProps, + buildDirectiveArgs, + PropsExpression } from './transforms/transformElement' export { processSlotOutlet } from './transforms/transformSlotOutlet' export { generateCodeFrame } from '@vue/shared' diff --git a/packages/compiler-core/src/transforms/transformElement.ts b/packages/compiler-core/src/transforms/transformElement.ts index 47cb3b0f48f..7143963d1f5 100644 --- a/packages/compiler-core/src/transforms/transformElement.ts +++ b/packages/compiler-core/src/transforms/transformElement.ts @@ -29,7 +29,8 @@ import { isObject, isReservedProp, capitalize, - camelize + camelize, + isBuiltInDirective } from '@vue/shared' import { createCompilerError, ErrorCodes } from '../errors' import { @@ -665,7 +666,7 @@ export function buildProps( directiveImportMap.set(prop, needRuntime) } } - } else { + } else if (!isBuiltInDirective(name)) { // no built-in transform, this is a user custom directive. runtimeDirectives.push(prop) // custom dirs may use beforeUpdate so they need to force blocks @@ -853,7 +854,7 @@ function mergeAsArray(existing: Property, incoming: Property) { } } -function buildDirectiveArgs( +export function buildDirectiveArgs( dir: DirectiveNode, context: TransformContext ): ArrayExpression { diff --git a/packages/compiler-dom/__tests__/transforms/__snapshots__/vModel.spec.ts.snap b/packages/compiler-dom/__tests__/transforms/__snapshots__/vModel.spec.ts.snap index 1a4ce5e66c7..cdaadc1d630 100644 --- a/packages/compiler-dom/__tests__/transforms/__snapshots__/vModel.spec.ts.snap +++ b/packages/compiler-dom/__tests__/transforms/__snapshots__/vModel.spec.ts.snap @@ -37,14 +37,11 @@ exports[`compiler: transform v-model input w/ dynamic v-bind 2`] = ` return function render(_ctx, _cache) { with (_ctx) { - const { vModelDynamic: _vModelDynamic, resolveDirective: _resolveDirective, withDirectives: _withDirectives, openBlock: _openBlock, createElementBlock: _createElementBlock } = _Vue - - const _directive_bind = _resolveDirective(\\"bind\\") + const { vModelDynamic: _vModelDynamic, withDirectives: _withDirectives, openBlock: _openBlock, createElementBlock: _createElementBlock } = _Vue return _withDirectives((_openBlock(), _createElementBlock(\\"input\\", { \\"onUpdate:modelValue\\": $event => ((model) = $event) }, null, 8 /* PROPS */, [\\"onUpdate:modelValue\\"])), [ - [_directive_bind, val, key], [_vModelDynamic, model] ]) } @@ -152,14 +149,11 @@ exports[`compiler: transform v-model simple expression for input (dynamic type) return function render(_ctx, _cache) { with (_ctx) { - const { vModelDynamic: _vModelDynamic, resolveDirective: _resolveDirective, withDirectives: _withDirectives, openBlock: _openBlock, createElementBlock: _createElementBlock } = _Vue - - const _directive_bind = _resolveDirective(\\"bind\\") + const { vModelDynamic: _vModelDynamic, withDirectives: _withDirectives, openBlock: _openBlock, createElementBlock: _createElementBlock } = _Vue return _withDirectives((_openBlock(), _createElementBlock(\\"input\\", { \\"onUpdate:modelValue\\": $event => ((model) = $event) }, null, 8 /* PROPS */, [\\"onUpdate:modelValue\\"])), [ - [_directive_bind, foo, \\"type\\"], [_vModelDynamic, model] ]) } diff --git a/packages/compiler-ssr/__tests__/ssrComponent.spec.ts b/packages/compiler-ssr/__tests__/ssrComponent.spec.ts index d7ceb0bcb3c..49eb9ff9941 100644 --- a/packages/compiler-ssr/__tests__/ssrComponent.spec.ts +++ b/packages/compiler-ssr/__tests__/ssrComponent.spec.ts @@ -377,4 +377,20 @@ describe('ssr: components', () => { }) }) }) + + describe('custom directive', () => { + test('basic', () => { + expect(compile(``).code).toMatchInlineSnapshot(` + "const { resolveComponent: _resolveComponent, resolveDirective: _resolveDirective, mergeProps: _mergeProps } = require(\\"vue\\") + const { ssrGetDirectiveProps: _ssrGetDirectiveProps, ssrRenderComponent: _ssrRenderComponent } = require(\\"vue/server-renderer\\") + + return function ssrRender(_ctx, _push, _parent, _attrs) { + const _component_foo = _resolveComponent(\\"foo\\") + const _directive_xxx = _resolveDirective(\\"xxx\\") + + _push(_ssrRenderComponent(_component_foo, _mergeProps(_attrs, _ssrGetDirectiveProps(_ctx, _directive_xxx, _ctx.z, \\"x\\", { y: true })), null, _parent)) + }" + `) + }) + }) }) diff --git a/packages/compiler-ssr/__tests__/ssrElement.spec.ts b/packages/compiler-ssr/__tests__/ssrElement.spec.ts index fee43975512..bf95065237d 100644 --- a/packages/compiler-ssr/__tests__/ssrElement.spec.ts +++ b/packages/compiler-ssr/__tests__/ssrElement.spec.ts @@ -288,5 +288,56 @@ describe('ssr: element', () => { }>
\`" `) }) + + test('custom dir', () => { + expect(getCompiledString(`
`)).toMatchInlineSnapshot(` + "\`
\`" + `) + }) + + test('custom dir with normal attrs', () => { + expect(getCompiledString(`
`)) + .toMatchInlineSnapshot(` + "\`
\`" + `) + }) + + test('custom dir with v-bind', () => { + expect(getCompiledString(`
`)) + .toMatchInlineSnapshot(` + "\`
\`" + `) + }) + + test('custom dir with object v-bind', () => { + expect(getCompiledString(`
`)) + .toMatchInlineSnapshot(` + "\`
\`" + `) + }) + + test('custom dir with object v-bind + normal bindings', () => { + expect( + getCompiledString(`
`) + ).toMatchInlineSnapshot(` + "\`
\`" + `) + }) }) }) diff --git a/packages/compiler-ssr/src/runtimeHelpers.ts b/packages/compiler-ssr/src/runtimeHelpers.ts index 9be6c610a93..f0a6a2f290c 100644 --- a/packages/compiler-ssr/src/runtimeHelpers.ts +++ b/packages/compiler-ssr/src/runtimeHelpers.ts @@ -17,6 +17,7 @@ export const SSR_RENDER_DYNAMIC_MODEL = Symbol(`ssrRenderDynamicModel`) export const SSR_GET_DYNAMIC_MODEL_PROPS = Symbol(`ssrGetDynamicModelProps`) export const SSR_RENDER_TELEPORT = Symbol(`ssrRenderTeleport`) export const SSR_RENDER_SUSPENSE = Symbol(`ssrRenderSuspense`) +export const SSR_GET_DIRECTIVE_PROPS = Symbol(`ssrGetDirectiveProps`) export const ssrHelpers = { [SSR_INTERPOLATE]: `ssrInterpolate`, @@ -35,7 +36,8 @@ export const ssrHelpers = { [SSR_RENDER_DYNAMIC_MODEL]: `ssrRenderDynamicModel`, [SSR_GET_DYNAMIC_MODEL_PROPS]: `ssrGetDynamicModelProps`, [SSR_RENDER_TELEPORT]: `ssrRenderTeleport`, - [SSR_RENDER_SUSPENSE]: `ssrRenderSuspense` + [SSR_RENDER_SUSPENSE]: `ssrRenderSuspense`, + [SSR_GET_DIRECTIVE_PROPS]: `ssrGetDirectiveProps` } // Note: these are helpers imported from @vue/server-renderer diff --git a/packages/compiler-ssr/src/transforms/ssrTransformComponent.ts b/packages/compiler-ssr/src/transforms/ssrTransformComponent.ts index e4cd2698d1b..b02d3afddb5 100644 --- a/packages/compiler-ssr/src/transforms/ssrTransformComponent.ts +++ b/packages/compiler-ssr/src/transforms/ssrTransformComponent.ts @@ -33,7 +33,8 @@ import { TELEPORT, TRANSITION_GROUP, CREATE_VNODE, - CallExpression + CallExpression, + JSChildNode } from '@vue/compiler-dom' import { SSR_RENDER_COMPONENT, SSR_RENDER_VNODE } from '../runtimeHelpers' import { @@ -48,6 +49,7 @@ import { } from './ssrTransformSuspense' import { ssrProcessTransitionGroup } from './ssrTransformTransitionGroup' import { isSymbol, isObject, isArray } from '@vue/shared' +import { buildSSRProps } from './ssrTransformElement' // We need to construct the slot functions in the 1st pass to ensure proper // scope tracking, but the children of each slot cannot be processed until @@ -110,12 +112,15 @@ export const ssrTransformComponent: NodeTransform = (node, context) => { }) } - const props = - node.props.length > 0 - ? // note we are not passing ssr: true here because for components, v-on - // handlers should still be passed - buildProps(node, context).props || `null` - : `null` + let propsExp: string | JSChildNode = `null` + if (node.props.length) { + // note we are not passing ssr: true here because for components, v-on + // handlers should still be passed + const { props, directives } = buildProps(node, context) + if (props || directives.length) { + propsExp = buildSSRProps(props, directives, context) + } + } const wipEntries: WIPSlotEntry[] = [] wipMap.set(node, wipEntries) @@ -151,7 +156,7 @@ export const ssrTransformComponent: NodeTransform = (node, context) => { `_push`, createCallExpression(context.helper(CREATE_VNODE), [ component, - props, + propsExp, slots ]), `_parent` @@ -160,7 +165,7 @@ export const ssrTransformComponent: NodeTransform = (node, context) => { } else { node.ssrCodegenNode = createCallExpression( context.helper(SSR_RENDER_COMPONENT), - [component, props, slots, `_parent`] + [component, propsExp, slots, `_parent`] ) } } diff --git a/packages/compiler-ssr/src/transforms/ssrTransformElement.ts b/packages/compiler-ssr/src/transforms/ssrTransformElement.ts index 13d8c04f4ce..08b7f4ad00b 100644 --- a/packages/compiler-ssr/src/transforms/ssrTransformElement.ts +++ b/packages/compiler-ssr/src/transforms/ssrTransformElement.ts @@ -26,11 +26,15 @@ import { createSequenceExpression, InterpolationNode, isStaticExp, - AttributeNode + AttributeNode, + buildDirectiveArgs, + TransformContext, + PropsExpression } from '@vue/compiler-dom' import { escapeHtml, isBooleanAttr, + isBuiltInDirective, isSSRSafeAttrName, NO, propsToAttrMap @@ -44,7 +48,8 @@ import { SSR_RENDER_ATTRS, SSR_INTERPOLATE, SSR_GET_DYNAMIC_MODEL_PROPS, - SSR_INCLUDE_BOOLEAN_ATTR + SSR_INCLUDE_BOOLEAN_ATTR, + SSR_GET_DIRECTIVE_PROPS } from '../runtimeHelpers' import { SSRTransformContext, processChildren } from '../ssrCodegenTransform' @@ -71,16 +76,26 @@ export const ssrTransformElement: NodeTransform = (node, context) => { const needTagForRuntime = node.tag === 'textarea' || node.tag.indexOf('-') > 0 - // v-bind="obj" or v-bind:[key] can potentially overwrite other static - // attrs and can affect final rendering result, so when they are present - // we need to bail out to full `renderAttrs` + // v-bind="obj", v-bind:[key] and custom directives can potentially + // overwrite other static attrs and can affect final rendering result, + // so when they are present we need to bail out to full `renderAttrs` const hasDynamicVBind = hasDynamicKeyVBind(node) - if (hasDynamicVBind) { - const { props } = buildProps(node, context, node.props, true /* ssr */) - if (props) { + const hasCustomDir = node.props.some( + p => p.type === NodeTypes.DIRECTIVE && !isBuiltInDirective(p.name) + ) + const needMergeProps = hasDynamicVBind || hasCustomDir + if (needMergeProps) { + const { props, directives } = buildProps( + node, + context, + node.props, + true /* ssr */ + ) + if (props || directives.length) { + const mergedProps = buildSSRProps(props, directives, context) const propsExp = createCallExpression( context.helper(SSR_RENDER_ATTRS), - [props] + [mergedProps] ) if (node.tag === 'textarea') { @@ -99,7 +114,7 @@ export const ssrTransformElement: NodeTransform = (node, context) => { propsExp.arguments = [ createAssignmentExpression( createSimpleExpression(tempId, false), - props + mergedProps ) ] rawChildrenMap.set( @@ -128,7 +143,7 @@ export const ssrTransformElement: NodeTransform = (node, context) => { const tempExp = createSimpleExpression(tempId, false) propsExp.arguments = [ createSequenceExpression([ - createAssignmentExpression(tempExp, props), + createAssignmentExpression(tempExp, mergedProps), createCallExpression(context.helper(MERGE_PROPS), [ tempExp, createCallExpression( @@ -176,10 +191,10 @@ export const ssrTransformElement: NodeTransform = (node, context) => { createCompilerError(ErrorCodes.X_V_SLOT_MISPLACED, prop.loc) ) } else if (isTextareaWithValue(node, prop) && prop.exp) { - if (!hasDynamicVBind) { + if (!needMergeProps) { node.children = [createInterpolation(prop.exp, prop.loc)] } - } else if (!hasDynamicVBind) { + } else if (!needMergeProps) { // Directive transforms. const directiveTransform = context.directiveTransforms[prop.name] if (directiveTransform) { @@ -277,7 +292,7 @@ export const ssrTransformElement: NodeTransform = (node, context) => { // special case: value on