wip(ssr): initial scaffold for compiler-ssr

This commit is contained in:
Evan You
2020-02-02 00:05:27 -05:00
parent 34e61197c7
commit efbbd19b3d
26 changed files with 496 additions and 36 deletions

View File

@@ -27,6 +27,6 @@
},
"homepage": "https://github.com/vuejs/vue/tree/dev/packages/compiler-ssr#readme",
"dependencies": {
"@vue/compiler-core": "3.0.0-alpha.4"
"@vue/compiler-dom": "3.0.0-alpha.4"
}
}

View File

@@ -0,0 +1,94 @@
import {
CodegenResult,
RootNode,
generate as baseGenerate,
CodegenOptions,
NodeTypes,
locStub,
BlockStatement,
ElementTypes,
createCallExpression,
TemplateLiteral,
createTemplateLiteral,
CallExpression,
TemplateChildNode
} from '@vue/compiler-dom'
import { isString } from '@vue/shared'
export function generate(
ast: RootNode,
options: CodegenOptions
): CodegenResult {
// construct a SSR-specific codegen tree to pass to core codegen
const body: BlockStatement['body'] = []
let currentCall: CallExpression | null = null
let currentString: TemplateLiteral | null = null
function ensureCurrentString() {
if (!currentCall) {
currentCall = createCallExpression(`_push`)
body.push(currentCall)
}
if (!currentString) {
currentString = createTemplateLiteral([])
currentCall.arguments.push(currentString)
}
return currentString.elements
}
function pushStringPart(part: TemplateLiteral['elements'][0]) {
const bufferedElements = ensureCurrentString()
const lastItem = bufferedElements[bufferedElements.length - 1]
if (isString(part) && isString(lastItem)) {
bufferedElements[bufferedElements.length - 1] += part
} else {
bufferedElements.push(part)
}
}
function processChildren(children: TemplateChildNode[]) {
for (let i = 0; i < children.length; i++) {
const child = children[i]
if (child.type === NodeTypes.ELEMENT) {
if (child.tagType === ElementTypes.ELEMENT) {
const elementsToAdd = child.ssrCodegenNode!.elements
for (let j = 0; j < elementsToAdd.length; j++) {
pushStringPart(elementsToAdd[j])
}
if (child.children.length) {
processChildren(child.children)
}
// push closing tag
pushStringPart(`</${child.tag}>`)
} else if (child.tagType === ElementTypes.COMPONENT) {
// TODO
} else if (child.tagType === ElementTypes.SLOT) {
// TODO
}
} else if (child.type === NodeTypes.TEXT) {
// TODO
} else if (child.type === NodeTypes.IF) {
// TODO
} else if (child.type === NodeTypes.FOR) {
// TODO
}
}
}
const isFragment = ast.children.length > 1
if (isFragment) {
pushStringPart(`<!---->`)
}
processChildren(ast.children)
if (isFragment) {
pushStringPart(`<!---->`)
}
ast.codegenNode = {
type: NodeTypes.JS_BLOCK_STATEMENT,
loc: locStub,
body
}
return baseGenerate(ast, options)
}

View File

@@ -1,3 +1,63 @@
export function hello(): string {
return 'TODO'
import {
CodegenResult,
baseParse,
parserOptions,
transform,
generate,
CompilerOptions,
transformExpression,
trackVForSlotScopes,
trackSlotScopes
} from '@vue/compiler-dom'
import { ssrCodegenTransform } from './ssrCodegenTransform'
import { ssrTransformIf } from './transforms/ssrVIf'
import { ssrTransformFor } from './transforms/ssrVFor'
import { ssrTransformElement } from './transforms/ssrTransformElement'
import { ssrTransformComponent } from './transforms/ssrTransformComponent'
import { ssrTransformSlotOutlet } from './transforms/ssrTransformSlotOutlet'
export interface SSRCompilerOptions extends CompilerOptions {}
export function compile(
template: string,
options: SSRCompilerOptions = {}
): CodegenResult {
const ast = baseParse(template, {
...parserOptions,
...options
})
transform(ast, {
...options,
prefixIdentifiers: true,
// disalbe optimizations that are unnecessary for ssr
cacheHandlers: false,
hoistStatic: false,
nodeTransforms: [
ssrTransformIf,
ssrTransformFor,
trackVForSlotScopes,
transformExpression,
ssrTransformSlotOutlet,
ssrTransformElement,
ssrTransformComponent,
trackSlotScopes,
...(options.nodeTransforms || []) // user transforms
],
directiveTransforms: {
// TODO server-side directive transforms
...(options.directiveTransforms || {}) // user transforms
}
})
// traverse the template AST and convert into SSR codegen AST
// by replacing ast.codegenNode.
ssrCodegenTransform(ast)
return generate(ast, {
mode: 'cjs',
...options,
ssr: true,
prefixIdentifiers: true
})
}

View File

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

View File

@@ -0,0 +1,99 @@
import {
RootNode,
BlockStatement,
CallExpression,
TemplateLiteral,
createCallExpression,
createTemplateLiteral,
locStub,
NodeTypes,
TemplateChildNode,
ElementTypes
} from '@vue/compiler-dom'
import { isString } from '@vue/shared'
// Because SSR codegen output is completely different from client-side output
// (e.g. multiple elements can be concatenated into a single template literal
// instead of each getting a corresponding call), we need to apply an extra
// transform pass to convert the template AST into a fresh JS AST before
// passing it to codegen.
export function ssrCodegenTransform(ast: RootNode) {
const context = createSSRTransformContext()
const isFragment = ast.children.length > 1
if (isFragment) {
context.pushStringPart(`<!---->`)
}
processChildren(ast.children, context)
if (isFragment) {
context.pushStringPart(`<!---->`)
}
ast.codegenNode = {
type: NodeTypes.JS_BLOCK_STATEMENT,
loc: locStub,
body: context.body
}
}
type SSRTransformContext = ReturnType<typeof createSSRTransformContext>
function createSSRTransformContext() {
const body: BlockStatement['body'] = []
let currentCall: CallExpression | null = null
let currentString: TemplateLiteral | null = null
return {
body,
pushStringPart(part: TemplateLiteral['elements'][0]) {
if (!currentCall) {
currentCall = createCallExpression(`_push`)
body.push(currentCall)
}
if (!currentString) {
currentString = createTemplateLiteral([])
currentCall.arguments.push(currentString)
}
const bufferedElements = currentString.elements
const lastItem = bufferedElements[bufferedElements.length - 1]
if (isString(part) && isString(lastItem)) {
bufferedElements[bufferedElements.length - 1] += part
} else {
bufferedElements.push(part)
}
}
}
}
function processChildren(
children: TemplateChildNode[],
context: SSRTransformContext
) {
for (let i = 0; i < children.length; i++) {
const child = children[i]
if (child.type === NodeTypes.ELEMENT) {
if (child.tagType === ElementTypes.ELEMENT) {
const elementsToAdd = child.ssrCodegenNode!.elements
for (let j = 0; j < elementsToAdd.length; j++) {
context.pushStringPart(elementsToAdd[j])
}
if (child.children.length) {
processChildren(child.children, context)
}
// push closing tag
context.pushStringPart(`</${child.tag}>`)
} else if (child.tagType === ElementTypes.COMPONENT) {
// TODO
} else if (child.tagType === ElementTypes.SLOT) {
// TODO
}
} else if (child.type === NodeTypes.TEXT) {
// TODO
} else if (child.type === NodeTypes.IF) {
// TODO
} else if (child.type === NodeTypes.FOR) {
// TODO
}
}
}

View File

@@ -0,0 +1,15 @@
import { NodeTransform, NodeTypes, ElementTypes } from '@vue/compiler-dom'
export const ssrTransformComponent: NodeTransform = (node, context) => {
if (
node.type === NodeTypes.ELEMENT &&
node.tagType === ElementTypes.COMPONENT
) {
return function ssrPostTransformComponent() {
// generate a _push(_renderComponent) call
// dynamic component as well
// !check if we need to bail out for slots
// TODO also handle scopeID here
}
}
}

View File

@@ -0,0 +1,70 @@
import {
NodeTransform,
NodeTypes,
ElementTypes,
TemplateLiteral,
createTemplateLiteral
} from '@vue/compiler-dom'
import { escapeHtml } from '@vue/server-renderer/src'
/*
## Simple Element
``` html
<div></div>
```
``` js
return function render(_ctx, _push, _parent) {
_push(`<div></div>`)
}
```
## Consecutive Elements
``` html
<div>
<span></span>
</div>
<div></div>
```
``` js
return function render(_ctx, _push, _parent) {
_push(`<div><span></span></div><div></div>`)
}
```
*/
export const ssrTransformElement: NodeTransform = (node, context) => {
if (
node.type === NodeTypes.ELEMENT &&
node.tagType === ElementTypes.ELEMENT
) {
return function ssrPostTransformElement() {
// element
// generate the template literal representing the open tag.
const openTag: TemplateLiteral['elements'] = [`<${node.tag}`]
for (let i = 0; i < node.props.length; i++) {
const prop = node.props[i]
if (prop.type === NodeTypes.DIRECTIVE) {
const directiveTransform = context.directiveTransforms[prop.name]
if (directiveTransform) {
// TODO directive transforms
} else {
// no corresponding ssr directive transform found.
// TODO emit error
}
} else {
// static prop
openTag.push(
` ${prop.name}` +
(prop.value ? `="${escapeHtml(prop.value.content)}"` : ``)
)
}
}
openTag.push(`>`)
node.ssrCodegenNode = createTemplateLiteral(openTag)
}
}
}

View File

@@ -0,0 +1,3 @@
import { NodeTransform } from '@vue/compiler-dom'
export const ssrTransformSlotOutlet: NodeTransform = () => {}

View File

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

View File

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

View File

@@ -0,0 +1,3 @@
import { NodeTransform } from '@vue/compiler-dom'
export const ssrTransformFor: NodeTransform = () => {}

View File

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

View File

@@ -0,0 +1,3 @@
import { NodeTransform } from '@vue/compiler-dom'
export const ssrTransformIf: NodeTransform = () => {}

View File

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

View File

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

View File

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

View File

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

View File

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