wip(ssr): initial scaffold for compiler-ssr
This commit is contained in:
parent
34e61197c7
commit
efbbd19b3d
@ -45,7 +45,12 @@ export const enum NodeTypes {
|
|||||||
JS_FUNCTION_EXPRESSION,
|
JS_FUNCTION_EXPRESSION,
|
||||||
JS_SEQUENCE_EXPRESSION,
|
JS_SEQUENCE_EXPRESSION,
|
||||||
JS_CONDITIONAL_EXPRESSION,
|
JS_CONDITIONAL_EXPRESSION,
|
||||||
JS_CACHE_EXPRESSION
|
JS_CACHE_EXPRESSION,
|
||||||
|
|
||||||
|
// ssr codegen
|
||||||
|
JS_BLOCK_STATEMENT,
|
||||||
|
JS_TEMPLATE_LITERAL,
|
||||||
|
JS_IF_STATEMENT
|
||||||
}
|
}
|
||||||
|
|
||||||
export const enum ElementTypes {
|
export const enum ElementTypes {
|
||||||
@ -97,7 +102,7 @@ export interface RootNode extends Node {
|
|||||||
hoists: JSChildNode[]
|
hoists: JSChildNode[]
|
||||||
imports: ImportItem[]
|
imports: ImportItem[]
|
||||||
cached: number
|
cached: number
|
||||||
codegenNode: TemplateChildNode | JSChildNode | undefined
|
codegenNode: TemplateChildNode | JSChildNode | BlockStatement | undefined
|
||||||
}
|
}
|
||||||
|
|
||||||
export type ElementNode =
|
export type ElementNode =
|
||||||
@ -130,6 +135,7 @@ export interface PlainElementNode extends BaseElementNode {
|
|||||||
| CacheExpression // when cached by v-once
|
| CacheExpression // when cached by v-once
|
||||||
| SequenceExpression // when turned into a block
|
| SequenceExpression // when turned into a block
|
||||||
| undefined
|
| undefined
|
||||||
|
ssrCodegenNode?: TemplateLiteral
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ComponentNode extends BaseElementNode {
|
export interface ComponentNode extends BaseElementNode {
|
||||||
@ -147,7 +153,7 @@ export interface SlotOutletNode extends BaseElementNode {
|
|||||||
|
|
||||||
export interface TemplateNode extends BaseElementNode {
|
export interface TemplateNode extends BaseElementNode {
|
||||||
tagType: ElementTypes.TEMPLATE
|
tagType: ElementTypes.TEMPLATE
|
||||||
codegenNode: ElementCodegenNode | undefined | CacheExpression
|
// TemplateNode is a container type that always gets compiled away
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface TextNode extends Node {
|
export interface TextNode extends Node {
|
||||||
@ -232,9 +238,12 @@ export interface TextCallNode extends Node {
|
|||||||
codegenNode: CallExpression
|
codegenNode: CallExpression
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// JS Node Types ---------------------------------------------------------------
|
||||||
|
|
||||||
// We also include a number of JavaScript AST nodes for code generation.
|
// We also include a number of JavaScript AST nodes for code generation.
|
||||||
// The AST is an intentionally minimal subset just to meet the exact needs of
|
// The AST is an intentionally minimal subset just to meet the exact needs of
|
||||||
// Vue render function generation.
|
// Vue render function generation.
|
||||||
|
|
||||||
export type JSChildNode =
|
export type JSChildNode =
|
||||||
| CallExpression
|
| CallExpression
|
||||||
| ObjectExpression
|
| ObjectExpression
|
||||||
@ -252,6 +261,7 @@ export interface CallExpression extends Node {
|
|||||||
| string
|
| string
|
||||||
| symbol
|
| symbol
|
||||||
| JSChildNode
|
| JSChildNode
|
||||||
|
| SSRCodegenNode
|
||||||
| TemplateChildNode
|
| TemplateChildNode
|
||||||
| TemplateChildNode[])[]
|
| TemplateChildNode[])[]
|
||||||
}
|
}
|
||||||
@ -275,8 +285,10 @@ export interface ArrayExpression extends Node {
|
|||||||
export interface FunctionExpression extends Node {
|
export interface FunctionExpression extends Node {
|
||||||
type: NodeTypes.JS_FUNCTION_EXPRESSION
|
type: NodeTypes.JS_FUNCTION_EXPRESSION
|
||||||
params: ExpressionNode | ExpressionNode[] | undefined
|
params: ExpressionNode | ExpressionNode[] | undefined
|
||||||
returns: TemplateChildNode | TemplateChildNode[] | JSChildNode
|
returns?: TemplateChildNode | TemplateChildNode[] | JSChildNode
|
||||||
|
body?: BlockStatement
|
||||||
newline: boolean
|
newline: boolean
|
||||||
|
// so that codegen knows it needs to generate ScopeId wrapper
|
||||||
isSlot: boolean
|
isSlot: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -299,6 +311,27 @@ export interface CacheExpression extends Node {
|
|||||||
isVNode: boolean
|
isVNode: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SSR-specific Node Types -----------------------------------------------------
|
||||||
|
|
||||||
|
export type SSRCodegenNode = BlockStatement | TemplateLiteral | IfStatement
|
||||||
|
|
||||||
|
export interface BlockStatement extends Node {
|
||||||
|
type: NodeTypes.JS_BLOCK_STATEMENT
|
||||||
|
body: (JSChildNode | IfStatement)[]
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface TemplateLiteral extends Node {
|
||||||
|
type: NodeTypes.JS_TEMPLATE_LITERAL
|
||||||
|
elements: (string | JSChildNode)[]
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IfStatement extends Node {
|
||||||
|
type: NodeTypes.JS_IF_STATEMENT
|
||||||
|
test: ExpressionNode
|
||||||
|
consequent: BlockStatement
|
||||||
|
alternate: IfStatement | BlockStatement
|
||||||
|
}
|
||||||
|
|
||||||
// Codegen Node Types ----------------------------------------------------------
|
// Codegen Node Types ----------------------------------------------------------
|
||||||
|
|
||||||
// createVNode(...)
|
// createVNode(...)
|
||||||
@ -637,3 +670,13 @@ export function createCacheExpression(
|
|||||||
loc: locStub
|
loc: locStub
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function createTemplateLiteral(
|
||||||
|
elements: TemplateLiteral['elements']
|
||||||
|
): TemplateLiteral {
|
||||||
|
return {
|
||||||
|
type: NodeTypes.JS_TEMPLATE_LITERAL,
|
||||||
|
elements,
|
||||||
|
loc: locStub
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -18,7 +18,9 @@ import {
|
|||||||
SequenceExpression,
|
SequenceExpression,
|
||||||
ConditionalExpression,
|
ConditionalExpression,
|
||||||
CacheExpression,
|
CacheExpression,
|
||||||
locStub
|
locStub,
|
||||||
|
SSRCodegenNode,
|
||||||
|
TemplateLiteral
|
||||||
} from './ast'
|
} from './ast'
|
||||||
import { SourceMapGenerator, RawSourceMap } from 'source-map'
|
import { SourceMapGenerator, RawSourceMap } from 'source-map'
|
||||||
import {
|
import {
|
||||||
@ -44,7 +46,7 @@ import {
|
|||||||
} from './runtimeHelpers'
|
} from './runtimeHelpers'
|
||||||
import { ImportItem } from './transform'
|
import { ImportItem } from './transform'
|
||||||
|
|
||||||
type CodegenNode = TemplateChildNode | JSChildNode
|
type CodegenNode = TemplateChildNode | JSChildNode | SSRCodegenNode
|
||||||
|
|
||||||
export interface CodegenResult {
|
export interface CodegenResult {
|
||||||
code: string
|
code: string
|
||||||
@ -74,7 +76,8 @@ function createCodegenContext(
|
|||||||
prefixIdentifiers = mode === 'module' || mode === 'cjs',
|
prefixIdentifiers = mode === 'module' || mode === 'cjs',
|
||||||
sourceMap = false,
|
sourceMap = false,
|
||||||
filename = `template.vue.html`,
|
filename = `template.vue.html`,
|
||||||
scopeId = null
|
scopeId = null,
|
||||||
|
ssr = false
|
||||||
}: CodegenOptions
|
}: CodegenOptions
|
||||||
): CodegenContext {
|
): CodegenContext {
|
||||||
const context: CodegenContext = {
|
const context: CodegenContext = {
|
||||||
@ -83,6 +86,7 @@ function createCodegenContext(
|
|||||||
sourceMap,
|
sourceMap,
|
||||||
filename,
|
filename,
|
||||||
scopeId,
|
scopeId,
|
||||||
|
ssr,
|
||||||
source: ast.loc.source,
|
source: ast.loc.source,
|
||||||
code: ``,
|
code: ``,
|
||||||
column: 1,
|
column: 1,
|
||||||
@ -169,7 +173,8 @@ export function generate(
|
|||||||
indent,
|
indent,
|
||||||
deindent,
|
deindent,
|
||||||
newline,
|
newline,
|
||||||
scopeId
|
scopeId,
|
||||||
|
ssr
|
||||||
} = context
|
} = context
|
||||||
const hasHelpers = ast.helpers.length > 0
|
const hasHelpers = ast.helpers.length > 0
|
||||||
const useWithBlock = !prefixIdentifiers && mode !== 'module'
|
const useWithBlock = !prefixIdentifiers && mode !== 'module'
|
||||||
@ -231,10 +236,14 @@ export function generate(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// enter render function
|
// enter render function
|
||||||
if (genScopeId) {
|
if (genScopeId && !ssr) {
|
||||||
push(`const render = withId(`)
|
push(`const render = withId(`)
|
||||||
}
|
}
|
||||||
push(`function render() {`)
|
if (!ssr) {
|
||||||
|
push(`function render() {`)
|
||||||
|
} else {
|
||||||
|
push(`function ssrRender(_ctx, _push, _parent) {`)
|
||||||
|
}
|
||||||
indent()
|
indent()
|
||||||
|
|
||||||
if (useWithBlock) {
|
if (useWithBlock) {
|
||||||
@ -255,7 +264,7 @@ export function generate(
|
|||||||
}
|
}
|
||||||
newline()
|
newline()
|
||||||
}
|
}
|
||||||
} else {
|
} else if (!ssr) {
|
||||||
push(`const _ctx = this`)
|
push(`const _ctx = this`)
|
||||||
if (ast.cached > 0) {
|
if (ast.cached > 0) {
|
||||||
newline()
|
newline()
|
||||||
@ -276,7 +285,9 @@ export function generate(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// generate the VNode tree expression
|
// generate the VNode tree expression
|
||||||
push(`return `)
|
if (!ssr) {
|
||||||
|
push(`return `)
|
||||||
|
}
|
||||||
if (ast.codegenNode) {
|
if (ast.codegenNode) {
|
||||||
genNode(ast.codegenNode, context)
|
genNode(ast.codegenNode, context)
|
||||||
} else {
|
} else {
|
||||||
@ -291,7 +302,7 @@ export function generate(
|
|||||||
deindent()
|
deindent()
|
||||||
push(`}`)
|
push(`}`)
|
||||||
|
|
||||||
if (genScopeId) {
|
if (genScopeId && !ssr) {
|
||||||
push(`)`)
|
push(`)`)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -325,7 +336,7 @@ function genHoists(hoists: JSChildNode[], context: CodegenContext) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
const { push, newline, helper, scopeId, mode } = context
|
const { push, newline, helper, scopeId, mode } = context
|
||||||
const genScopeId = !__BROWSER__ && scopeId != null && mode === 'module'
|
const genScopeId = !__BROWSER__ && scopeId != null && mode !== 'function'
|
||||||
newline()
|
newline()
|
||||||
|
|
||||||
// push scope Id before initilaizing hoisted vnodes so that these vnodes
|
// push scope Id before initilaizing hoisted vnodes so that these vnodes
|
||||||
@ -469,6 +480,18 @@ function genNode(node: CodegenNode | symbol | string, context: CodegenContext) {
|
|||||||
case NodeTypes.JS_CACHE_EXPRESSION:
|
case NodeTypes.JS_CACHE_EXPRESSION:
|
||||||
genCacheExpression(node, context)
|
genCacheExpression(node, context)
|
||||||
break
|
break
|
||||||
|
|
||||||
|
// SSR only types
|
||||||
|
case NodeTypes.JS_BLOCK_STATEMENT:
|
||||||
|
!__BROWSER__ && genNodeList(node.body, context, true)
|
||||||
|
break
|
||||||
|
case NodeTypes.JS_TEMPLATE_LITERAL:
|
||||||
|
!__BROWSER__ && genTemplateLiteral(node, context)
|
||||||
|
break
|
||||||
|
case NodeTypes.JS_IF_STATEMENT:
|
||||||
|
// TODO
|
||||||
|
break
|
||||||
|
|
||||||
/* istanbul ignore next */
|
/* istanbul ignore next */
|
||||||
default:
|
default:
|
||||||
if (__DEV__) {
|
if (__DEV__) {
|
||||||
@ -589,10 +612,10 @@ function genFunctionExpression(
|
|||||||
context: CodegenContext
|
context: CodegenContext
|
||||||
) {
|
) {
|
||||||
const { push, indent, deindent, scopeId, mode } = context
|
const { push, indent, deindent, scopeId, mode } = context
|
||||||
const { params, returns, newline, isSlot } = node
|
const { params, returns, body, newline, isSlot } = node
|
||||||
// slot functions also need to push scopeId before rendering its content
|
// slot functions also need to push scopeId before rendering its content
|
||||||
const genScopeId =
|
const genScopeId =
|
||||||
!__BROWSER__ && isSlot && scopeId != null && mode === 'module'
|
!__BROWSER__ && isSlot && scopeId != null && mode !== 'function'
|
||||||
|
|
||||||
if (genScopeId) {
|
if (genScopeId) {
|
||||||
push(`withId(`)
|
push(`withId(`)
|
||||||
@ -604,17 +627,23 @@ function genFunctionExpression(
|
|||||||
genNode(params, context)
|
genNode(params, context)
|
||||||
}
|
}
|
||||||
push(`) => `)
|
push(`) => `)
|
||||||
if (newline) {
|
if (newline || body) {
|
||||||
push(`{`)
|
push(`{`)
|
||||||
indent()
|
indent()
|
||||||
push(`return `)
|
|
||||||
}
|
}
|
||||||
if (isArray(returns)) {
|
if (returns) {
|
||||||
genNodeListAsArray(returns, context)
|
if (newline) {
|
||||||
} else {
|
push(`return `)
|
||||||
genNode(returns, context)
|
}
|
||||||
|
if (isArray(returns)) {
|
||||||
|
genNodeListAsArray(returns, context)
|
||||||
|
} else {
|
||||||
|
genNode(returns, context)
|
||||||
|
}
|
||||||
|
} else if (body) {
|
||||||
|
genNode(body, context)
|
||||||
}
|
}
|
||||||
if (newline) {
|
if (newline || body) {
|
||||||
deindent()
|
deindent()
|
||||||
push(`}`)
|
push(`}`)
|
||||||
}
|
}
|
||||||
@ -686,3 +715,19 @@ function genCacheExpression(node: CacheExpression, context: CodegenContext) {
|
|||||||
}
|
}
|
||||||
push(`)`)
|
push(`)`)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function genTemplateLiteral(node: TemplateLiteral, context: CodegenContext) {
|
||||||
|
const { push } = context
|
||||||
|
push('`')
|
||||||
|
for (let i = 0; i < node.elements.length; i++) {
|
||||||
|
const e = node.elements[i]
|
||||||
|
if (isString(e)) {
|
||||||
|
push(e.replace(/`/g, '\\`'))
|
||||||
|
} else {
|
||||||
|
push('${')
|
||||||
|
genNode(e, context)
|
||||||
|
push('}')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
push('`')
|
||||||
|
}
|
||||||
|
@ -31,6 +31,11 @@ export { registerRuntimeHelpers } from './runtimeHelpers'
|
|||||||
export { transformModel } from './transforms/vModel'
|
export { transformModel } from './transforms/vModel'
|
||||||
export { transformOn } from './transforms/vOn'
|
export { transformOn } from './transforms/vOn'
|
||||||
|
|
||||||
|
// exported for compiler-ssr
|
||||||
|
export { transformExpression } from './transforms/transformExpression'
|
||||||
|
export { trackVForSlotScopes, trackSlotScopes } from './transforms/vSlot'
|
||||||
|
export { buildProps } from './transforms/transformElement'
|
||||||
|
|
||||||
// utility, but need to rewrite typing to avoid dts relying on @vue/shared
|
// utility, but need to rewrite typing to avoid dts relying on @vue/shared
|
||||||
import { generateCodeFrame as _genCodeFrame } from '@vue/shared'
|
import { generateCodeFrame as _genCodeFrame } from '@vue/shared'
|
||||||
const generateCodeFrame = _genCodeFrame as (
|
const generateCodeFrame = _genCodeFrame as (
|
||||||
|
@ -29,7 +29,7 @@ export interface ParserOptions {
|
|||||||
|
|
||||||
export interface TransformOptions {
|
export interface TransformOptions {
|
||||||
nodeTransforms?: NodeTransform[]
|
nodeTransforms?: NodeTransform[]
|
||||||
directiveTransforms?: { [name: string]: DirectiveTransform }
|
directiveTransforms?: { [name: 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`.
|
||||||
// - This is force-enabled in module mode, since modules are by default strict
|
// - This is force-enabled in module mode, since modules are by default strict
|
||||||
@ -76,6 +76,8 @@ export interface CodegenOptions {
|
|||||||
filename?: string
|
filename?: string
|
||||||
// SFC scoped styles ID
|
// SFC scoped styles ID
|
||||||
scopeId?: string | null
|
scopeId?: string | null
|
||||||
|
// generate SSR specific code?
|
||||||
|
ssr?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
export type CompilerOptions = ParserOptions & TransformOptions & CodegenOptions
|
export type CompilerOptions = ParserOptions & TransformOptions & CodegenOptions
|
||||||
|
@ -18,7 +18,9 @@ import { transformOn } from './transforms/vOn'
|
|||||||
import { transformShow } from './transforms/vShow'
|
import { transformShow } from './transforms/vShow'
|
||||||
import { TRANSITION, TRANSITION_GROUP } from './runtimeHelpers'
|
import { TRANSITION, TRANSITION_GROUP } from './runtimeHelpers'
|
||||||
|
|
||||||
const parserOptions = __BROWSER__ ? parserOptionsMinimal : parserOptionsStandard
|
export const parserOptions = __BROWSER__
|
||||||
|
? parserOptionsMinimal
|
||||||
|
: parserOptionsStandard
|
||||||
|
|
||||||
export function compile(
|
export function compile(
|
||||||
template: string,
|
template: string,
|
||||||
|
@ -27,6 +27,6 @@
|
|||||||
},
|
},
|
||||||
"homepage": "https://github.com/vuejs/vue/tree/dev/packages/compiler-ssr#readme",
|
"homepage": "https://github.com/vuejs/vue/tree/dev/packages/compiler-ssr#readme",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@vue/compiler-core": "3.0.0-alpha.4"
|
"@vue/compiler-dom": "3.0.0-alpha.4"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
94
packages/compiler-ssr/src/codegen.ts
Normal file
94
packages/compiler-ssr/src/codegen.ts
Normal 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)
|
||||||
|
}
|
@ -1,3 +1,63 @@
|
|||||||
export function hello(): string {
|
import {
|
||||||
return 'TODO'
|
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
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
1
packages/compiler-ssr/src/runtimeHelpers.ts
Normal file
1
packages/compiler-ssr/src/runtimeHelpers.ts
Normal file
@ -0,0 +1 @@
|
|||||||
|
//
|
99
packages/compiler-ssr/src/ssrCodegenTransform.ts
Normal file
99
packages/compiler-ssr/src/ssrCodegenTransform.ts
Normal 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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
70
packages/compiler-ssr/src/transforms/ssrTransformElement.ts
Normal file
70
packages/compiler-ssr/src/transforms/ssrTransformElement.ts
Normal 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,3 @@
|
|||||||
|
import { NodeTransform } from '@vue/compiler-dom'
|
||||||
|
|
||||||
|
export const ssrTransformSlotOutlet: NodeTransform = () => {}
|
1
packages/compiler-ssr/src/transforms/ssrVBind.ts
Normal file
1
packages/compiler-ssr/src/transforms/ssrVBind.ts
Normal file
@ -0,0 +1 @@
|
|||||||
|
// TODO
|
1
packages/compiler-ssr/src/transforms/ssrVCloak.ts
Normal file
1
packages/compiler-ssr/src/transforms/ssrVCloak.ts
Normal file
@ -0,0 +1 @@
|
|||||||
|
// TODO
|
3
packages/compiler-ssr/src/transforms/ssrVFor.ts
Normal file
3
packages/compiler-ssr/src/transforms/ssrVFor.ts
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
import { NodeTransform } from '@vue/compiler-dom'
|
||||||
|
|
||||||
|
export const ssrTransformFor: NodeTransform = () => {}
|
1
packages/compiler-ssr/src/transforms/ssrVHtml.ts
Normal file
1
packages/compiler-ssr/src/transforms/ssrVHtml.ts
Normal file
@ -0,0 +1 @@
|
|||||||
|
// TODO
|
3
packages/compiler-ssr/src/transforms/ssrVIf.ts
Normal file
3
packages/compiler-ssr/src/transforms/ssrVIf.ts
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
import { NodeTransform } from '@vue/compiler-dom'
|
||||||
|
|
||||||
|
export const ssrTransformIf: NodeTransform = () => {}
|
1
packages/compiler-ssr/src/transforms/ssrVModel.ts
Normal file
1
packages/compiler-ssr/src/transforms/ssrVModel.ts
Normal file
@ -0,0 +1 @@
|
|||||||
|
// TODO
|
1
packages/compiler-ssr/src/transforms/ssrVOn.ts
Normal file
1
packages/compiler-ssr/src/transforms/ssrVOn.ts
Normal file
@ -0,0 +1 @@
|
|||||||
|
// TODO
|
1
packages/compiler-ssr/src/transforms/ssrVShow.ts
Normal file
1
packages/compiler-ssr/src/transforms/ssrVShow.ts
Normal file
@ -0,0 +1 @@
|
|||||||
|
// TODO
|
1
packages/compiler-ssr/src/transforms/ssrVSlot.ts
Normal file
1
packages/compiler-ssr/src/transforms/ssrVSlot.ts
Normal file
@ -0,0 +1 @@
|
|||||||
|
// TODO
|
1
packages/compiler-ssr/src/transforms/ssrVText.ts
Normal file
1
packages/compiler-ssr/src/transforms/ssrVText.ts
Normal file
@ -0,0 +1 @@
|
|||||||
|
// TODO
|
@ -146,10 +146,11 @@ describe('ssr: renderToString', () => {
|
|||||||
{ msg: 'hello' },
|
{ msg: 'hello' },
|
||||||
{
|
{
|
||||||
// optimized slot using string push
|
// optimized slot using string push
|
||||||
default: ({ msg }: any, push: any) => {
|
default: ({ msg }: any, push: any, p: any) => {
|
||||||
push(`<span>${msg}</span>`)
|
push(`<span>${msg}</span>`)
|
||||||
},
|
},
|
||||||
_compiled: true // important to avoid slots being normalized
|
// important to avoid slots being normalized
|
||||||
|
_compiled: true as any
|
||||||
},
|
},
|
||||||
parent
|
parent
|
||||||
)
|
)
|
||||||
|
@ -1,3 +1,7 @@
|
|||||||
export { renderToString, renderComponent, renderSlot } from './renderToString'
|
// public
|
||||||
|
export { renderToString } from './renderToString'
|
||||||
|
|
||||||
|
// internal
|
||||||
|
export { renderComponent, renderSlot } from './renderToString'
|
||||||
export { renderClass, renderStyle, renderProps } from './renderProps'
|
export { renderClass, renderStyle, renderProps } from './renderProps'
|
||||||
export { escapeHtml, interpolate } from './ssrUtils'
|
export { escapeHtml, interpolate } from './ssrUtils'
|
||||||
|
@ -4,7 +4,6 @@ import {
|
|||||||
ComponentInternalInstance,
|
ComponentInternalInstance,
|
||||||
VNode,
|
VNode,
|
||||||
VNodeArrayChildren,
|
VNodeArrayChildren,
|
||||||
VNodeNormalizedChildren,
|
|
||||||
createVNode,
|
createVNode,
|
||||||
Text,
|
Text,
|
||||||
Comment,
|
Comment,
|
||||||
@ -12,7 +11,8 @@ import {
|
|||||||
Portal,
|
Portal,
|
||||||
ShapeFlags,
|
ShapeFlags,
|
||||||
ssrUtils,
|
ssrUtils,
|
||||||
Slot
|
Slot,
|
||||||
|
Slots
|
||||||
} from 'vue'
|
} from 'vue'
|
||||||
import {
|
import {
|
||||||
isString,
|
isString,
|
||||||
@ -99,7 +99,7 @@ export async function renderToString(input: App | VNode): Promise<string> {
|
|||||||
export function renderComponent(
|
export function renderComponent(
|
||||||
comp: Component,
|
comp: Component,
|
||||||
props: Props | null = null,
|
props: Props | null = null,
|
||||||
children: VNodeNormalizedChildren | null = null,
|
children: Slots | SSRSlots | null = null,
|
||||||
parentComponent: ComponentInternalInstance | null = null
|
parentComponent: ComponentInternalInstance | null = null
|
||||||
): ResolvedSSRBuffer | Promise<ResolvedSSRBuffer> {
|
): ResolvedSSRBuffer | Promise<ResolvedSSRBuffer> {
|
||||||
return renderComponentVNode(
|
return renderComponentVNode(
|
||||||
@ -256,14 +256,16 @@ function renderElement(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
type OptimizedSlotFn = (
|
export type SSRSlots = Record<string, SSRSlot>
|
||||||
|
|
||||||
|
export type SSRSlot = (
|
||||||
props: Props,
|
props: Props,
|
||||||
push: PushFn,
|
push: PushFn,
|
||||||
parentComponent: ComponentInternalInstance | null
|
parentComponent: ComponentInternalInstance | null
|
||||||
) => void
|
) => void
|
||||||
|
|
||||||
export function renderSlot(
|
export function renderSlot(
|
||||||
slotFn: Slot | OptimizedSlotFn,
|
slotFn: Slot | SSRSlot,
|
||||||
slotProps: Props,
|
slotProps: Props,
|
||||||
push: PushFn,
|
push: PushFn,
|
||||||
parentComponent: ComponentInternalInstance | null = null
|
parentComponent: ComponentInternalInstance | null = null
|
||||||
|
Loading…
x
Reference in New Issue
Block a user