From c059fc88b9fb68a8096510344b6986658336cc54 Mon Sep 17 00:00:00 2001 From: Evan You Date: Tue, 4 Feb 2020 16:47:12 -0500 Subject: [PATCH] wip(compiler-ssr): v-bind with static keys --- packages/compiler-core/src/ast.ts | 7 +- packages/compiler-core/src/codegen.ts | 18 ++-- packages/compiler-core/src/index.ts | 1 + .../compiler-ssr/__tests__/ssrElement.spec.ts | 86 +++++++++++++------ .../compiler-ssr/__tests__/ssrText.spec.ts | 6 +- .../compiler-ssr/__tests__/ssrVBind.spec.ts | 13 --- .../compiler-ssr/__tests__/ssrVFor.spec.ts | 12 ++- packages/compiler-ssr/__tests__/utils.ts | 2 +- packages/compiler-ssr/src/errors.ts | 6 +- packages/compiler-ssr/src/index.ts | 14 +-- packages/compiler-ssr/src/runtimeHelpers.ts | 2 + .../src/transforms/ssrTransformElement.ts | 71 +++++++++++++-- .../compiler-ssr/src/transforms/ssrVBind.ts | 18 ---- .../compiler-ssr/src/transforms/ssrVModel.ts | 2 +- .../compiler-ssr/src/transforms/ssrVShow.ts | 2 +- ...enderProps.spec.ts => renderAttrs.spec.ts} | 0 .../src/helpers/renderAttrs.ts | 18 +++- packages/server-renderer/src/index.ts | 3 +- 18 files changed, 189 insertions(+), 92 deletions(-) delete mode 100644 packages/compiler-ssr/__tests__/ssrVBind.spec.ts delete mode 100644 packages/compiler-ssr/src/transforms/ssrVBind.ts rename packages/server-renderer/__tests__/{renderProps.spec.ts => renderAttrs.spec.ts} (100%) diff --git a/packages/compiler-core/src/ast.ts b/packages/compiler-core/src/ast.ts index d35530d6..eb824a00 100644 --- a/packages/compiler-core/src/ast.ts +++ b/packages/compiler-core/src/ast.ts @@ -301,9 +301,10 @@ export interface SequenceExpression extends Node { export interface ConditionalExpression extends Node { type: NodeTypes.JS_CONDITIONAL_EXPRESSION - test: ExpressionNode + test: JSChildNode consequent: JSChildNode alternate: JSChildNode + newline: boolean } export interface CacheExpression extends Node { @@ -648,13 +649,15 @@ export function createSequenceExpression( export function createConditionalExpression( test: ConditionalExpression['test'], consequent: ConditionalExpression['consequent'], - alternate: ConditionalExpression['alternate'] + alternate: ConditionalExpression['alternate'], + newline = true ): ConditionalExpression { return { type: NodeTypes.JS_CONDITIONAL_EXPRESSION, test, consequent, alternate, + newline, loc: locStub } } diff --git a/packages/compiler-core/src/codegen.ts b/packages/compiler-core/src/codegen.ts index e609541e..b0acb69d 100644 --- a/packages/compiler-core/src/codegen.ts +++ b/packages/compiler-core/src/codegen.ts @@ -685,7 +685,7 @@ function genConditionalExpression( node: ConditionalExpression, context: CodegenContext ) { - const { test, consequent, alternate } = node + const { test, consequent, alternate, newline: needNewline } = node const { push, indent, deindent, newline } = context if (test.type === NodeTypes.SIMPLE_EXPRESSION) { const needsParens = !isSimpleIdentifier(test.content) @@ -694,15 +694,15 @@ function genConditionalExpression( needsParens && push(`)`) } else { push(`(`) - genCompoundExpression(test, context) + genNode(test, context) push(`)`) } - indent() + needNewline && indent() context.indentLevel++ push(`? `) genNode(consequent, context) context.indentLevel-- - newline() + needNewline && newline() push(`: `) const isNested = alternate.type === NodeTypes.JS_CONDITIONAL_EXPRESSION if (!isNested) { @@ -712,7 +712,7 @@ function genConditionalExpression( if (!isNested) { context.indentLevel-- } - deindent(true /* without newline */) + needNewline && deindent(true /* without newline */) } function genSequenceExpression( @@ -748,15 +748,17 @@ function genCacheExpression(node: CacheExpression, context: CodegenContext) { function genTemplateLiteral(node: TemplateLiteral, context: CodegenContext) { const { push, indent, deindent } = context push('`') - for (let i = 0; i < node.elements.length; i++) { + const l = node.elements.length + const multilines = l > 3 + for (let i = 0; i < l; i++) { const e = node.elements[i] if (isString(e)) { push(e.replace(/`/g, '\\`')) } else { push('${') - indent() + if (multilines) indent() genNode(e, context) - deindent() + if (multilines) deindent() push('}') } } diff --git a/packages/compiler-core/src/index.ts b/packages/compiler-core/src/index.ts index bb4a23d4..07d1a405 100644 --- a/packages/compiler-core/src/index.ts +++ b/packages/compiler-core/src/index.ts @@ -31,6 +31,7 @@ export { noopDirectiveTransform } from './transforms/noopDirectiveTransform' // expose transforms so higher-order compilers can import and extend them export { transformModel } from './transforms/vModel' export { transformOn } from './transforms/vOn' +export { transformBind } from './transforms/vBind' // exported for compiler-ssr export { processIfBranches } from './transforms/vIf' diff --git a/packages/compiler-ssr/__tests__/ssrElement.spec.ts b/packages/compiler-ssr/__tests__/ssrElement.spec.ts index 521f1531..994fbeb8 100644 --- a/packages/compiler-ssr/__tests__/ssrElement.spec.ts +++ b/packages/compiler-ssr/__tests__/ssrElement.spec.ts @@ -10,12 +10,6 @@ describe('ssr: element', () => { ) }) - test('static attrs', () => { - expect( - getCompiledString(`
`) - ).toMatchInlineSnapshot(`"\`
\`"`) - }) - test('nested elements', () => { expect( getCompiledString(`
`) @@ -26,27 +20,71 @@ describe('ssr: element', () => { expect(getCompiledString(``)).toMatchInlineSnapshot(`"\`\`"`) }) - test('v-html', () => { - expect(getCompiledString(`
`)).toMatchInlineSnapshot( - `"\`
\${_ctx.foo}
\`"` - ) + describe('children override', () => { + test('v-html', () => { + expect(getCompiledString(`
`)).toMatchInlineSnapshot( + `"\`
\${_ctx.foo}
\`"` + ) + }) + + test('v-text', () => { + expect(getCompiledString(`
`)).toMatchInlineSnapshot( + `"\`
\${_interpolate(_ctx.foo)}
\`"` + ) + }) + + test('\`"` + ) + }) + + test('\`"`) + }) }) - test('v-text', () => { - expect(getCompiledString(`
`)).toMatchInlineSnapshot( - `"\`
\${_interpolate(_ctx.foo)}
\`"` - ) - }) + describe('attrs', () => { + test('static attrs', () => { + expect( + getCompiledString(`
`) + ).toMatchInlineSnapshot(`"\`
\`"`) + }) - test('\`"` - ) - }) + test('v-bind:class', () => { + expect( + getCompiledString(`
`) + ).toMatchInlineSnapshot( + `"\`
\`"` + ) + }) - test('\`"`) + test('v-bind:style', () => { + expect( + getCompiledString(`
`) + ).toMatchInlineSnapshot( + `"\`
\`"` + ) + }) + + test('v-bind:key (boolean)', () => { + expect( + getCompiledString(``) + ).toMatchInlineSnapshot( + `"\`\`"` + ) + }) + + test('v-bind:key (non-boolean)', () => { + expect( + getCompiledString(`
`) + ).toMatchInlineSnapshot( + `"\`
\`"` + ) + }) }) }) diff --git a/packages/compiler-ssr/__tests__/ssrText.spec.ts b/packages/compiler-ssr/__tests__/ssrText.spec.ts index 7ca7cded..5626a630 100644 --- a/packages/compiler-ssr/__tests__/ssrText.spec.ts +++ b/packages/compiler-ssr/__tests__/ssrText.spec.ts @@ -38,7 +38,11 @@ describe('ssr: text', () => { "const { _interpolate } = require(\\"@vue/server-renderer\\") return function ssrRender(_ctx, _push, _parent) { - _push(\`
\${_interpolate(_ctx.foo)} barbaz \${_interpolate(_ctx.qux)}
\`) + _push(\`
\${ + _interpolate(_ctx.foo) + } barbaz \${ + _interpolate(_ctx.qux) + }
\`) }" `) }) diff --git a/packages/compiler-ssr/__tests__/ssrVBind.spec.ts b/packages/compiler-ssr/__tests__/ssrVBind.spec.ts deleted file mode 100644 index 83c049fb..00000000 --- a/packages/compiler-ssr/__tests__/ssrVBind.spec.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { compile } from '../src' - -describe('ssr: v-bind', () => { - test('basic', () => { - expect(compile(`
`).code).toMatchInlineSnapshot(` - "const { _renderAttr } = require(\\"vue\\") - - return function ssrRender(_ctx, _push, _parent) { - _push(\`
\`) - }" - `) - }) -}) diff --git a/packages/compiler-ssr/__tests__/ssrVFor.spec.ts b/packages/compiler-ssr/__tests__/ssrVFor.spec.ts index ac11021e..064abd06 100644 --- a/packages/compiler-ssr/__tests__/ssrVFor.spec.ts +++ b/packages/compiler-ssr/__tests__/ssrVFor.spec.ts @@ -45,7 +45,11 @@ describe('ssr: v-for', () => { _renderList(_ctx.list, (row, i) => { _push(\`
\`) _renderList(row, (j) => { - _push(\`
\${_interpolate(i)},\${_interpolate(j)}
\`) + _push(\`
\${ + _interpolate(i) + },\${ + _interpolate(j) + }
\`) }) _push(\`
\`) }) @@ -97,7 +101,11 @@ describe('ssr: v-for', () => { return function ssrRender(_ctx, _push, _parent) { _push(\`\`) _renderList(_ctx.list, (i) => { - _push(\`\${_interpolate(i)}\${_interpolate(i + 1)}\`) + _push(\`\${ + _interpolate(i) + }\${ + _interpolate(i + 1) + }\`) }) _push(\`\`) }" diff --git a/packages/compiler-ssr/__tests__/utils.ts b/packages/compiler-ssr/__tests__/utils.ts index 333b1e1c..cc3cd45b 100644 --- a/packages/compiler-ssr/__tests__/utils.ts +++ b/packages/compiler-ssr/__tests__/utils.ts @@ -1,5 +1,5 @@ import { compile } from '../src' export function getCompiledString(src: string): string { - return compile(src).code.match(/_push\((.*)\)/)![1] + return compile(src).code.match(/_push\(([^]*)\)/)![1] } diff --git a/packages/compiler-ssr/src/errors.ts b/packages/compiler-ssr/src/errors.ts index 375bea68..30431fb0 100644 --- a/packages/compiler-ssr/src/errors.ts +++ b/packages/compiler-ssr/src/errors.ts @@ -17,9 +17,11 @@ export function createSSRCompilerError( } export const enum SSRErrorCodes { - X_SSR_CUSTOM_DIRECTIVE_NO_TRANSFORM = DOMErrorCodes.__EXTEND_POINT__ + X_SSR_CUSTOM_DIRECTIVE_NO_TRANSFORM = DOMErrorCodes.__EXTEND_POINT__, + X_SSR_UNSAFE_ATTR_NAME } export const SSRErrorMessages: { [code: number]: string } = { - [SSRErrorCodes.X_SSR_CUSTOM_DIRECTIVE_NO_TRANSFORM]: `Custom directive is missing corresponding SSR transform and will be ignored.` + [SSRErrorCodes.X_SSR_CUSTOM_DIRECTIVE_NO_TRANSFORM]: `Custom directive is missing corresponding SSR transform and will be ignored.`, + [SSRErrorCodes.X_SSR_UNSAFE_ATTR_NAME]: `Unsafe attribute name for SSR.` } diff --git a/packages/compiler-ssr/src/index.ts b/packages/compiler-ssr/src/index.ts index b8833e07..295c923a 100644 --- a/packages/compiler-ssr/src/index.ts +++ b/packages/compiler-ssr/src/index.ts @@ -8,7 +8,8 @@ import { transformExpression, trackVForSlotScopes, trackSlotScopes, - noopDirectiveTransform + noopDirectiveTransform, + transformBind } from '@vue/compiler-dom' import { ssrCodegenTransform } from './ssrCodegenTransform' import { ssrTransformElement } from './transforms/ssrTransformElement' @@ -16,9 +17,8 @@ import { ssrTransformComponent } from './transforms/ssrTransformComponent' import { ssrTransformSlotOutlet } from './transforms/ssrTransformSlotOutlet' import { ssrTransformIf } from './transforms/ssrVIf' import { ssrTransformFor } from './transforms/ssrVFor' -import { ssrVBind } from './transforms/ssrVBind' -import { ssrVModel } from './transforms/ssrVModel' -import { ssrVShow } from './transforms/ssrVShow' +import { ssrTransformModel } from './transforms/ssrVModel' +import { ssrTransformShow } from './transforms/ssrVShow' export function compile( template: string, @@ -54,9 +54,9 @@ export function compile( ssrDirectiveTransforms: { on: noopDirectiveTransform, cloak: noopDirectiveTransform, - bind: ssrVBind, - model: ssrVModel, - show: ssrVShow, + bind: transformBind, // reusing core v-bind + model: ssrTransformModel, + show: ssrTransformShow, ...(options.ssrDirectiveTransforms || {}) // user transforms } }) diff --git a/packages/compiler-ssr/src/runtimeHelpers.ts b/packages/compiler-ssr/src/runtimeHelpers.ts index 06df6b25..6c98e3ce 100644 --- a/packages/compiler-ssr/src/runtimeHelpers.ts +++ b/packages/compiler-ssr/src/runtimeHelpers.ts @@ -7,6 +7,7 @@ export const SSR_RENDER_CLASS = Symbol(`renderClass`) export const SSR_RENDER_STYLE = Symbol(`renderStyle`) export const SSR_RENDER_ATTRS = Symbol(`renderAttrs`) export const SSR_RENDER_ATTR = Symbol(`renderAttr`) +export const SSR_RENDER_DYNAMIC_ATTR = Symbol(`renderDynamicAttr`) export const SSR_RENDER_LIST = Symbol(`renderList`) // Note: these are helpers imported from @vue/server-renderer @@ -19,5 +20,6 @@ registerRuntimeHelpers({ [SSR_RENDER_STYLE]: `_renderStyle`, [SSR_RENDER_ATTRS]: `_renderAttrs`, [SSR_RENDER_ATTR]: `_renderAttr`, + [SSR_RENDER_DYNAMIC_ATTR]: `_renderDynamicAttr`, [SSR_RENDER_LIST]: `_renderList` }) diff --git a/packages/compiler-ssr/src/transforms/ssrTransformElement.ts b/packages/compiler-ssr/src/transforms/ssrTransformElement.ts index 6d89ed62..7d864782 100644 --- a/packages/compiler-ssr/src/transforms/ssrTransformElement.ts +++ b/packages/compiler-ssr/src/transforms/ssrTransformElement.ts @@ -5,11 +5,18 @@ import { TemplateLiteral, createTemplateLiteral, createInterpolation, - createCallExpression + createCallExpression, + createConditionalExpression, + createSimpleExpression } from '@vue/compiler-dom' -import { escapeHtml } from '@vue/shared' +import { escapeHtml, isBooleanAttr, isSSRSafeAttrName } from '@vue/shared' import { createSSRCompilerError, SSRErrorCodes } from '../errors' -import { SSR_RENDER_ATTR } from '../runtimeHelpers' +import { + SSR_RENDER_ATTR, + SSR_RENDER_CLASS, + SSR_RENDER_STYLE, + SSR_RENDER_DYNAMIC_ATTR +} from '../runtimeHelpers' export const ssrTransformElement: NodeTransform = (node, context) => { if ( @@ -66,12 +73,58 @@ export const ssrTransformElement: NodeTransform = (node, context) => { const { props } = directiveTransform(prop, node, context) for (let j = 0; j < props.length; j++) { const { key, value } = props[j] - openTag.push( - createCallExpression(context.helper(SSR_RENDER_ATTR), [ - key, - value - ]) - ) + if (key.type === NodeTypes.SIMPLE_EXPRESSION && key.isStatic) { + const attrName = key.content + // static key attr + if (attrName === 'class') { + openTag.push( + createCallExpression(context.helper(SSR_RENDER_CLASS), [ + value + ]) + ) + } else if (attrName === 'style') { + openTag.push( + createCallExpression(context.helper(SSR_RENDER_STYLE), [ + value + ]) + ) + } else if (isBooleanAttr(attrName)) { + openTag.push( + createConditionalExpression( + value, + createSimpleExpression(' ' + attrName, true), + createSimpleExpression('', true), + false /* no newline */ + ) + ) + } else { + if (isSSRSafeAttrName(attrName)) { + openTag.push( + createCallExpression(context.helper(SSR_RENDER_ATTR), [ + key, + value + ]) + ) + } else { + context.onError( + createSSRCompilerError( + SSRErrorCodes.X_SSR_UNSAFE_ATTR_NAME, + key.loc + ) + ) + } + } + } else { + // dynamic key attr + // this branch is only encountered for custom directive + // transforms that returns properties with dynamic keys + openTag.push( + createCallExpression( + context.helper(SSR_RENDER_DYNAMIC_ATTR), + [key, value] + ) + ) + } } } else { // no corresponding ssr directive transform found. diff --git a/packages/compiler-ssr/src/transforms/ssrVBind.ts b/packages/compiler-ssr/src/transforms/ssrVBind.ts deleted file mode 100644 index 072d54c3..00000000 --- a/packages/compiler-ssr/src/transforms/ssrVBind.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { DirectiveTransform, createObjectProperty } from '@vue/compiler-dom' - -export const ssrVBind: DirectiveTransform = (dir, node, context) => { - if (!dir.exp) { - // error - return { props: [] } - } else { - // TODO modifiers - return { - props: [ - createObjectProperty( - dir.arg!, // v-bind="obj" is handled separately - dir.exp - ) - ] - } - } -} diff --git a/packages/compiler-ssr/src/transforms/ssrVModel.ts b/packages/compiler-ssr/src/transforms/ssrVModel.ts index b7e59be7..077991d8 100644 --- a/packages/compiler-ssr/src/transforms/ssrVModel.ts +++ b/packages/compiler-ssr/src/transforms/ssrVModel.ts @@ -1,6 +1,6 @@ import { DirectiveTransform } from '@vue/compiler-dom' -export const ssrVModel: DirectiveTransform = (dir, node, context) => { +export const ssrTransformModel: DirectiveTransform = (dir, node, context) => { return { props: [] } diff --git a/packages/compiler-ssr/src/transforms/ssrVShow.ts b/packages/compiler-ssr/src/transforms/ssrVShow.ts index eced5115..b5671b51 100644 --- a/packages/compiler-ssr/src/transforms/ssrVShow.ts +++ b/packages/compiler-ssr/src/transforms/ssrVShow.ts @@ -1,6 +1,6 @@ import { DirectiveTransform } from '@vue/compiler-dom' -export const ssrVShow: DirectiveTransform = (dir, node, context) => { +export const ssrTransformShow: DirectiveTransform = (dir, node, context) => { return { props: [] } diff --git a/packages/server-renderer/__tests__/renderProps.spec.ts b/packages/server-renderer/__tests__/renderAttrs.spec.ts similarity index 100% rename from packages/server-renderer/__tests__/renderProps.spec.ts rename to packages/server-renderer/__tests__/renderAttrs.spec.ts diff --git a/packages/server-renderer/src/helpers/renderAttrs.ts b/packages/server-renderer/src/helpers/renderAttrs.ts index 4549903c..eaeb9e9b 100644 --- a/packages/server-renderer/src/helpers/renderAttrs.ts +++ b/packages/server-renderer/src/helpers/renderAttrs.ts @@ -33,13 +33,18 @@ export function renderAttrs( } else if (key === 'style') { ret += ` style="${renderStyle(value)}"` } else { - ret += renderAttr(key, value, tag) + ret += renderDynamicAttr(key, value, tag) } } return ret } -export function renderAttr(key: string, value: unknown, tag?: string): string { +// render an attr with dynamic (unknown) key. +export function renderDynamicAttr( + key: string, + value: unknown, + tag?: string +): string { if (value == null) { return `` } @@ -56,6 +61,15 @@ export function renderAttr(key: string, value: unknown, tag?: string): string { } } +// Render a v-bind attr with static key. The key is pre-processed at compile +// time and we only need to check and escape value. +export function renderAttr(key: string, value: unknown): string { + if (value == null) { + return `` + } + return ` ${key}="${escapeHtml(value)}"` +} + export function renderClass(raw: unknown): string { return escapeHtml(normalizeClass(raw)) } diff --git a/packages/server-renderer/src/index.ts b/packages/server-renderer/src/index.ts index 2d4c2884..39e5c3e3 100644 --- a/packages/server-renderer/src/index.ts +++ b/packages/server-renderer/src/index.ts @@ -10,7 +10,8 @@ export { renderClass as _renderClass, renderStyle as _renderStyle, renderAttrs as _renderAttrs, - renderAttr as _renderAttr + renderAttr as _renderAttr, + renderDynamicAttr as _renderDynamicAttr } from './helpers/renderAttrs' export { interpolate as _interpolate } from './helpers/interpolate' export { renderList as _renderList } from './helpers/renderList'