wip(ssr): v-bind basic usage

This commit is contained in:
Evan You 2020-02-04 12:20:51 -05:00
parent 7f38c1e0ff
commit 6a5ed49ea9
25 changed files with 173 additions and 75 deletions

View File

@ -0,0 +1,26 @@
import {
baseParse as parse,
transform,
ElementNode,
CallExpression,
noopDirectiveTransform
} from '../../src'
import { transformElement } from '../../src/transforms/transformElement'
describe('compiler: noop directive transform', () => {
test('should add no props to DOM', () => {
const ast = parse(`<div v-noop/>`)
transform(ast, {
nodeTransforms: [transformElement],
directiveTransforms: {
noop: noopDirectiveTransform
}
})
const node = ast.children[0] as ElementNode
const codegenArgs = (node.codegenNode as CallExpression).arguments
// As v-noop adds no properties the codegen should be identical to
// rendering a div with no props or reactive data (so just the tag as the arg)
expect(codegenArgs.length).toBe(1)
})
})

View File

@ -405,8 +405,7 @@ describe('compiler: element transform', () => {
foo(dir) { foo(dir) {
_dir = dir _dir = dir
return { return {
props: [createObjectProperty(dir.arg!, dir.exp!)], props: [createObjectProperty(dir.arg!, dir.exp!)]
needRuntime: false
} }
} }
} }

View File

@ -26,6 +26,7 @@ export {
export * from './ast' export * from './ast'
export * from './utils' export * from './utils'
export { registerRuntimeHelpers } from './runtimeHelpers' export { registerRuntimeHelpers } from './runtimeHelpers'
export { noopDirectiveTransform } from './transforms/noopDirectiveTransform'
// expose transforms so higher-order compilers can import and extend them // expose transforms so higher-order compilers can import and extend them
export { transformModel } from './transforms/vModel' export { transformModel } from './transforms/vModel'

View File

@ -28,7 +28,8 @@ export interface ParserOptions {
export interface TransformOptions { export interface TransformOptions {
nodeTransforms?: NodeTransform[] nodeTransforms?: NodeTransform[]
directiveTransforms?: { [name: string]: DirectiveTransform | undefined } directiveTransforms?: Record<string, DirectiveTransform | undefined>
ssrDirectiveTransforms?: Record<string, DirectiveTransform | undefined>
isBuiltInComponent?: (tag: string) => symbol | void isBuiltInComponent?: (tag: string) => symbol | void
// Transform expressions like {{ foo }} to `_ctx.foo`. // Transform expressions like {{ foo }} to `_ctx.foo`.
// If this option is false, the generated code will be wrapped in a // If this option is false, the generated code will be wrapped in a

View File

@ -61,7 +61,7 @@ export type DirectiveTransform = (
export interface DirectiveTransformResult { export interface DirectiveTransformResult {
props: Property[] props: Property[]
needRuntime: boolean | symbol needRuntime?: boolean | symbol
} }
// A structural directive transform is a technically a NodeTransform; // A structural directive transform is a technically a NodeTransform;
@ -114,6 +114,7 @@ function createTransformContext(
cacheHandlers = false, cacheHandlers = false,
nodeTransforms = [], nodeTransforms = [],
directiveTransforms = {}, directiveTransforms = {},
ssrDirectiveTransforms = {},
isBuiltInComponent = NOOP, isBuiltInComponent = NOOP,
ssr = false, ssr = false,
onError = defaultOnError onError = defaultOnError
@ -126,6 +127,7 @@ function createTransformContext(
cacheHandlers, cacheHandlers,
nodeTransforms, nodeTransforms,
directiveTransforms, directiveTransforms,
ssrDirectiveTransforms,
isBuiltInComponent, isBuiltInComponent,
ssr, ssr,
onError, onError,

View File

@ -0,0 +1,3 @@
import { DirectiveTransform } from '../transform'
export const noopDirectiveTransform: DirectiveTransform = () => ({ props: [] })

View File

@ -30,7 +30,6 @@ export const transformBind: DirectiveTransform = (dir, node, context) => {
return { return {
props: [ props: [
createObjectProperty(arg!, exp || createSimpleExpression('', true, loc)) createObjectProperty(arg!, exp || createSimpleExpression('', true, loc))
], ]
needRuntime: false
} }
} }

View File

@ -101,5 +101,5 @@ export const transformModel: DirectiveTransform = (dir, node, context) => {
} }
function createTransformProps(props: Property[] = []) { function createTransformProps(props: Property[] = []) {
return { props, needRuntime: false } return { props }
} }

View File

@ -99,8 +99,7 @@ export const transformOn: DirectiveTransform = (
eventName, eventName,
exp || createSimpleExpression(`() => {}`, false, loc) exp || createSimpleExpression(`() => {}`, false, loc)
) )
], ]
needRuntime: false
} }
// apply extended compiler augmentor // apply extended compiler augmentor

View File

@ -1,30 +0,0 @@
import {
baseParse as parse,
transform,
ElementNode,
CallExpression
} from '@vue/compiler-core'
import { transformCloak } from '../../src/transforms/vCloak'
import { transformElement } from '../../../compiler-core/src/transforms/transformElement'
function transformWithCloak(template: string) {
const ast = parse(template)
transform(ast, {
nodeTransforms: [transformElement],
directiveTransforms: {
cloak: transformCloak
}
})
return ast.children[0] as ElementNode
}
describe('compiler: v-cloak transform', () => {
test('should add no props to DOM', () => {
const node = transformWithCloak(`<div v-cloak/>`)
const codegenArgs = (node.codegenNode as CallExpression).arguments
// As v-cloak adds no properties the codegen should be identical to
// rendering a div with no props or reactive data (so just the tag as the arg)
expect(codegenArgs.length).toBe(1)
})
})

View File

@ -28,7 +28,8 @@ export const enum DOMErrorCodes {
X_V_MODEL_ON_INVALID_ELEMENT, X_V_MODEL_ON_INVALID_ELEMENT,
X_V_MODEL_ARG_ON_ELEMENT, X_V_MODEL_ARG_ON_ELEMENT,
X_V_MODEL_ON_FILE_INPUT_ELEMENT, X_V_MODEL_ON_FILE_INPUT_ELEMENT,
X_V_SHOW_NO_EXPRESSION X_V_SHOW_NO_EXPRESSION,
__EXTEND_POINT__
} }
export const DOMErrorMessages: { [code: number]: string } = { export const DOMErrorMessages: { [code: number]: string } = {

View File

@ -5,12 +5,12 @@ import {
CodegenResult, CodegenResult,
isBuiltInType, isBuiltInType,
ParserOptions, ParserOptions,
RootNode RootNode,
noopDirectiveTransform
} from '@vue/compiler-core' } from '@vue/compiler-core'
import { parserOptionsMinimal } from './parserOptionsMinimal' import { parserOptionsMinimal } from './parserOptionsMinimal'
import { parserOptionsStandard } from './parserOptionsStandard' import { parserOptionsStandard } from './parserOptionsStandard'
import { transformStyle } from './transforms/transformStyle' import { transformStyle } from './transforms/transformStyle'
import { transformCloak } from './transforms/vCloak'
import { transformVHtml } from './transforms/vHtml' import { transformVHtml } from './transforms/vHtml'
import { transformVText } from './transforms/vText' import { transformVText } from './transforms/vText'
import { transformModel } from './transforms/vModel' import { transformModel } from './transforms/vModel'
@ -31,7 +31,7 @@ export function compile(
...options, ...options,
nodeTransforms: [transformStyle, ...(options.nodeTransforms || [])], nodeTransforms: [transformStyle, ...(options.nodeTransforms || [])],
directiveTransforms: { directiveTransforms: {
cloak: transformCloak, cloak: noopDirectiveTransform,
html: transformVHtml, html: transformVHtml,
text: transformVText, text: transformVText,
model: transformModel, // override compiler-core model: transformModel, // override compiler-core
@ -56,4 +56,5 @@ export function parse(template: string, options: ParserOptions = {}): RootNode {
}) })
} }
export { DOMErrorCodes } from './errors'
export * from '@vue/compiler-core' export * from '@vue/compiler-core'

View File

@ -1,5 +0,0 @@
import { DirectiveTransform } from '@vue/compiler-core'
export const transformCloak: DirectiveTransform = () => {
return { props: [], needRuntime: false }
}

View File

@ -24,7 +24,6 @@ export const transformVHtml: DirectiveTransform = (dir, node, context) => {
createSimpleExpression(`innerHTML`, true, loc), createSimpleExpression(`innerHTML`, true, loc),
exp || createSimpleExpression('', true) exp || createSimpleExpression('', true)
) )
], ]
needRuntime: false
} }
} }

View File

@ -102,8 +102,7 @@ export const transformOn: DirectiveTransform = (dir, node, context) => {
} }
return { return {
props: [createObjectProperty(key, handlerExp)], props: [createObjectProperty(key, handlerExp)]
needRuntime: false
} }
}) })
} }

View File

@ -24,7 +24,6 @@ export const transformVText: DirectiveTransform = (dir, node, context) => {
createSimpleExpression(`textContent`, true, loc), createSimpleExpression(`textContent`, true, loc),
exp || createSimpleExpression('', true) exp || createSimpleExpression('', true)
) )
], ]
needRuntime: false
} }
} }

View File

@ -0,0 +1,13 @@
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>\`)
}"
`)
})
})

View File

@ -0,0 +1,25 @@
import {
SourceLocation,
CompilerError,
createCompilerError,
DOMErrorCodes
} from '@vue/compiler-dom'
export interface SSRCompilerError extends CompilerError {
code: SSRErrorCodes
}
export function createSSRCompilerError(
code: SSRErrorCodes,
loc?: SourceLocation
): SSRCompilerError {
return createCompilerError(code, loc, SSRErrorMessages)
}
export const enum SSRErrorCodes {
X_SSR_CUSTOM_DIRECTIVE_NO_TRANSFORM = DOMErrorCodes.__EXTEND_POINT__
}
export const SSRErrorMessages: { [code: number]: string } = {
[SSRErrorCodes.X_SSR_CUSTOM_DIRECTIVE_NO_TRANSFORM]: `Custom directive is missing corresponding SSR transform and will be ignored.`
}

View File

@ -7,20 +7,22 @@ import {
CompilerOptions, CompilerOptions,
transformExpression, transformExpression,
trackVForSlotScopes, trackVForSlotScopes,
trackSlotScopes trackSlotScopes,
noopDirectiveTransform
} from '@vue/compiler-dom' } from '@vue/compiler-dom'
import { ssrCodegenTransform } from './ssrCodegenTransform' import { ssrCodegenTransform } from './ssrCodegenTransform'
import { ssrTransformIf } from './transforms/ssrVIf'
import { ssrTransformFor } from './transforms/ssrVFor'
import { ssrTransformElement } from './transforms/ssrTransformElement' import { ssrTransformElement } from './transforms/ssrTransformElement'
import { ssrTransformComponent } from './transforms/ssrTransformComponent' import { ssrTransformComponent } from './transforms/ssrTransformComponent'
import { ssrTransformSlotOutlet } from './transforms/ssrTransformSlotOutlet' import { ssrTransformSlotOutlet } from './transforms/ssrTransformSlotOutlet'
import { ssrTransformIf } from './transforms/ssrVIf'
export interface SSRCompilerOptions extends CompilerOptions {} import { ssrTransformFor } from './transforms/ssrVFor'
import { ssrVBind } from './transforms/ssrVBind'
import { ssrVModel } from './transforms/ssrVModel'
import { ssrVShow } from './transforms/ssrVShow'
export function compile( export function compile(
template: string, template: string,
options: SSRCompilerOptions = {} options: CompilerOptions = {}
): CodegenResult { ): CodegenResult {
options = { options = {
mode: 'cjs', mode: 'cjs',
@ -50,9 +52,13 @@ export function compile(
trackSlotScopes, trackSlotScopes,
...(options.nodeTransforms || []) // user transforms ...(options.nodeTransforms || []) // user transforms
], ],
directiveTransforms: { ssrDirectiveTransforms: {
// TODO server-side directive transforms on: noopDirectiveTransform,
...(options.directiveTransforms || {}) // user transforms cloak: noopDirectiveTransform,
bind: ssrVBind,
model: ssrVModel,
show: ssrVShow,
...(options.ssrDirectiveTransforms || {}) // user transforms
} }
}) })

View File

@ -4,9 +4,12 @@ import {
ElementTypes, ElementTypes,
TemplateLiteral, TemplateLiteral,
createTemplateLiteral, createTemplateLiteral,
createInterpolation createInterpolation,
createCallExpression
} from '@vue/compiler-dom' } from '@vue/compiler-dom'
import { escapeHtml } from '@vue/shared' import { escapeHtml } from '@vue/shared'
import { createSSRCompilerError, SSRErrorCodes } from '../errors'
import { SSR_RENDER_ATTR } from '../runtimeHelpers'
export const ssrTransformElement: NodeTransform = (node, context) => { export const ssrTransformElement: NodeTransform = (node, context) => {
if ( if (
@ -19,10 +22,25 @@ export const ssrTransformElement: NodeTransform = (node, context) => {
const openTag: TemplateLiteral['elements'] = [`<${node.tag}`] const openTag: TemplateLiteral['elements'] = [`<${node.tag}`]
let rawChildren let rawChildren
// 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`
const hasDynamicVBind = node.props.some(
p =>
p.type === NodeTypes.DIRECTIVE &&
p.name === 'bind' &&
(!p.arg || // v-bind="obj"
p.arg.type !== NodeTypes.SIMPLE_EXPRESSION || // v-bind:[_ctx.foo]
!p.arg.isStatic) // v-bind:[foo]
)
if (hasDynamicVBind) {
}
for (let i = 0; i < node.props.length; i++) { for (let i = 0; i < node.props.length; i++) {
const prop = node.props[i] const prop = node.props[i]
// special cases with children override
if (prop.type === NodeTypes.DIRECTIVE) { if (prop.type === NodeTypes.DIRECTIVE) {
// special cases with children override
if (prop.name === 'html' && prop.exp) { if (prop.name === 'html' && prop.exp) {
node.children = [] node.children = []
rawChildren = prop.exp rawChildren = prop.exp
@ -40,13 +58,28 @@ export const ssrTransformElement: NodeTransform = (node, context) => {
) { ) {
node.children = [createInterpolation(prop.exp, prop.loc)] node.children = [createInterpolation(prop.exp, prop.loc)]
// TODO handle <textrea> with dynamic v-bind // TODO handle <textrea> with dynamic v-bind
} else { } else if (!hasDynamicVBind) {
const directiveTransform = context.directiveTransforms[prop.name] // Directive transforms.
const directiveTransform = context.ssrDirectiveTransforms[prop.name]
if (directiveTransform) { if (directiveTransform) {
// TODO directive transforms const { props } = directiveTransform(prop, node, context)
for (let j = 0; j < props.length; j++) {
const { key, value } = props[i]
openTag.push(
createCallExpression(context.helper(SSR_RENDER_ATTR), [
key,
value
])
)
}
} else { } else {
// no corresponding ssr directive transform found. // no corresponding ssr directive transform found.
// TODO emit error context.onError(
createSSRCompilerError(
SSRErrorCodes.X_SSR_CUSTOM_DIRECTIVE_NO_TRANSFORM,
prop.loc
)
)
} }
} }
} else { } else {
@ -54,7 +87,7 @@ export const ssrTransformElement: NodeTransform = (node, context) => {
if (node.tag === 'textarea' && prop.name === 'value' && prop.value) { if (node.tag === 'textarea' && prop.name === 'value' && prop.value) {
node.children = [] node.children = []
rawChildren = escapeHtml(prop.value.content) rawChildren = escapeHtml(prop.value.content)
} else { } else if (!hasDynamicVBind) {
// static prop // static prop
openTag.push( openTag.push(
` ${prop.name}` + ` ${prop.name}` +

View File

@ -1 +1,18 @@
// TODO 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
)
]
}
}
}

View File

@ -1 +0,0 @@
// TODO

View File

@ -1 +1,7 @@
// TODO import { DirectiveTransform } from '@vue/compiler-dom'
export const ssrVModel: DirectiveTransform = (dir, node, context) => {
return {
props: []
}
}

View File

@ -1 +0,0 @@
// TODO

View File

@ -1 +1,7 @@
// TODO import { DirectiveTransform } from '@vue/compiler-dom'
export const ssrVShow: DirectiveTransform = (dir, node, context) => {
return {
props: []
}
}