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 {
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
}
}
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)
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) {
if (!isNested) {
context.indentLevel--
}
- deindent(true /* without newline */)
+ needNewline && deindent(true /* without newline */)
}
function genSequenceExpression(
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('}')
}
}
// 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'
)
})
- test('static attrs', () => {
- expect(
- getCompiledString(`<div id="foo" class="bar"></div>`)
- ).toMatchInlineSnapshot(`"\`<div id=\\"foo\\" class=\\"bar\\"></div>\`"`)
- })
-
test('nested elements', () => {
expect(
getCompiledString(`<div><span></span><span></span></div>`)
expect(getCompiledString(`<input>`)).toMatchInlineSnapshot(`"\`<input>\`"`)
})
- test('v-html', () => {
- expect(getCompiledString(`<div v-html="foo"/>`)).toMatchInlineSnapshot(
- `"\`<div>\${_ctx.foo}</div>\`"`
- )
- })
+ describe('children override', () => {
+ test('v-html', () => {
+ expect(getCompiledString(`<div v-html="foo"/>`)).toMatchInlineSnapshot(
+ `"\`<div>\${_ctx.foo}</div>\`"`
+ )
+ })
- test('v-text', () => {
- expect(getCompiledString(`<div v-text="foo"/>`)).toMatchInlineSnapshot(
- `"\`<div>\${_interpolate(_ctx.foo)}</div>\`"`
- )
- })
+ test('v-text', () => {
+ expect(getCompiledString(`<div v-text="foo"/>`)).toMatchInlineSnapshot(
+ `"\`<div>\${_interpolate(_ctx.foo)}</div>\`"`
+ )
+ })
- test('<textarea> with dynamic value', () => {
- expect(getCompiledString(`<textarea :value="foo"/>`)).toMatchInlineSnapshot(
- `"\`<textarea>\${_interpolate(_ctx.foo)}</textarea>\`"`
- )
+ test('<textarea> with dynamic value', () => {
+ expect(
+ getCompiledString(`<textarea :value="foo"/>`)
+ ).toMatchInlineSnapshot(
+ `"\`<textarea>\${_interpolate(_ctx.foo)}</textarea>\`"`
+ )
+ })
+
+ test('<textarea> with static value', () => {
+ expect(
+ getCompiledString(`<textarea value="fo>o"/>`)
+ ).toMatchInlineSnapshot(`"\`<textarea>fo>o</textarea>\`"`)
+ })
})
- test('<textarea> with static value', () => {
- expect(
- getCompiledString(`<textarea value="fo>o"/>`)
- ).toMatchInlineSnapshot(`"\`<textarea>fo>o</textarea>\`"`)
+ describe('attrs', () => {
+ test('static attrs', () => {
+ expect(
+ getCompiledString(`<div id="foo" class="bar"></div>`)
+ ).toMatchInlineSnapshot(`"\`<div id=\\"foo\\" class=\\"bar\\"></div>\`"`)
+ })
+
+ test('v-bind:class', () => {
+ expect(
+ getCompiledString(`<div id="foo" :class="bar"></div>`)
+ ).toMatchInlineSnapshot(
+ `"\`<div id=\\"foo\\"\${_renderClass(_ctx.bar)}></div>\`"`
+ )
+ })
+
+ test('v-bind:style', () => {
+ expect(
+ getCompiledString(`<div id="foo" :style="bar"></div>`)
+ ).toMatchInlineSnapshot(
+ `"\`<div id=\\"foo\\"\${_renderStyle(_ctx.bar)}></div>\`"`
+ )
+ })
+
+ test('v-bind:key (boolean)', () => {
+ expect(
+ getCompiledString(`<input type="checkbox" :checked="checked">`)
+ ).toMatchInlineSnapshot(
+ `"\`<input type=\\"checkbox\\"\${(_ctx.checked)? \\" checked\\": \\"\\"}>\`"`
+ )
+ })
+
+ test('v-bind:key (non-boolean)', () => {
+ expect(
+ getCompiledString(`<div :id="id" class="bar"></div>`)
+ ).toMatchInlineSnapshot(
+ `"\`<div\${_renderAttr(\\"id\\", _ctx.id)} class=\\"bar\\"></div>\`"`
+ )
+ })
})
})
"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>\`)
+ _push(\`<div><span>\${
+ _interpolate(_ctx.foo)
+ } bar</span><span>baz \${
+ _interpolate(_ctx.qux)
+ }</span></div>\`)
}"
`)
})
+++ /dev/null
-import { compile } from '../src'
-
-describe('ssr: v-bind', () => {
- test('basic', () => {
- expect(compile(`<div :id="id"/>`).code).toMatchInlineSnapshot(`
- "const { _renderAttr } = require(\\"vue\\")
-
- return function ssrRender(_ctx, _push, _parent) {
- _push(\`<div\${_renderAttr(\\"id\\", _ctx.id)}></div>\`)
- }"
- `)
- })
-})
_renderList(_ctx.list, (row, i) => {
_push(\`<div><!---->\`)
_renderList(row, (j) => {
- _push(\`<div>\${_interpolate(i)},\${_interpolate(j)}</div>\`)
+ _push(\`<div>\${
+ _interpolate(i)
+ },\${
+ _interpolate(j)
+ }</div>\`)
})
_push(\`<!----></div>\`)
})
return function ssrRender(_ctx, _push, _parent) {
_push(\`<!---->\`)
_renderList(_ctx.list, (i) => {
- _push(\`<!----><span>\${_interpolate(i)}</span><span>\${_interpolate(i + 1)}</span><!---->\`)
+ _push(\`<!----><span>\${
+ _interpolate(i)
+ }</span><span>\${
+ _interpolate(i + 1)
+ }</span><!---->\`)
})
_push(\`<!---->\`)
}"
import { compile } from '../src'
export function getCompiledString(src: string): string {
- return compile(src).code.match(/_push\((.*)\)/)![1]
+ return compile(src).code.match(/_push\(([^]*)\)/)![1]
}
}
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.`
}
transformExpression,
trackVForSlotScopes,
trackSlotScopes,
- noopDirectiveTransform
+ noopDirectiveTransform,
+ transformBind
} from '@vue/compiler-dom'
import { ssrCodegenTransform } from './ssrCodegenTransform'
import { ssrTransformElement } from './transforms/ssrTransformElement'
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,
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
}
})
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
[SSR_RENDER_STYLE]: `_renderStyle`,
[SSR_RENDER_ATTRS]: `_renderAttrs`,
[SSR_RENDER_ATTR]: `_renderAttr`,
+ [SSR_RENDER_DYNAMIC_ATTR]: `_renderDynamicAttr`,
[SSR_RENDER_LIST]: `_renderList`
})
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 (
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.
+++ /dev/null
-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
- )
- ]
- }
- }
-}
import { DirectiveTransform } from '@vue/compiler-dom'
-export const ssrVModel: DirectiveTransform = (dir, node, context) => {
+export const ssrTransformModel: DirectiveTransform = (dir, node, context) => {
return {
props: []
}
import { DirectiveTransform } from '@vue/compiler-dom'
-export const ssrVShow: DirectiveTransform = (dir, node, context) => {
+export const ssrTransformShow: DirectiveTransform = (dir, node, context) => {
return {
props: []
}
} 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 ``
}
}
}
+// 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))
}
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'