feat(compiler): transformStyle + context.hoist
This commit is contained in:
@@ -68,6 +68,7 @@ export interface RootNode extends Node {
|
||||
children: ChildNode[]
|
||||
imports: string[]
|
||||
statements: string[]
|
||||
hoists: JSChildNode[]
|
||||
}
|
||||
|
||||
export interface ElementNode extends Node {
|
||||
|
||||
@@ -148,14 +148,16 @@ export function generate(
|
||||
if (mode === 'function') {
|
||||
// generate const declarations for helpers
|
||||
if (imports) {
|
||||
push(`const { ${imports} } = Vue\n\n`)
|
||||
push(`const { ${imports} } = Vue\n`)
|
||||
}
|
||||
genHoists(ast.hoists, context)
|
||||
push(`return `)
|
||||
} else {
|
||||
// generate import statements for helpers
|
||||
if (imports) {
|
||||
push(`import { ${imports} } from 'vue'\n\n`)
|
||||
push(`import { ${imports} } from 'vue'\n`)
|
||||
}
|
||||
genHoists(ast.hoists, context)
|
||||
push(`export default `)
|
||||
}
|
||||
push(`function render() {`)
|
||||
@@ -190,6 +192,15 @@ export function generate(
|
||||
}
|
||||
}
|
||||
|
||||
function genHoists(hoists: JSChildNode[], context: CodegenContext) {
|
||||
hoists.forEach((exp, i) => {
|
||||
context.push(`const _hoisted_${i + 1} = `)
|
||||
genNode(exp, context)
|
||||
context.newline()
|
||||
})
|
||||
context.newline()
|
||||
}
|
||||
|
||||
// This will generate a single vnode call if:
|
||||
// - The list has length === 1, AND:
|
||||
// - This is a root node, OR:
|
||||
|
||||
@@ -10,6 +10,7 @@ import { transformOn } from './transforms/vOn'
|
||||
import { transformBind } from './transforms/vBind'
|
||||
import { transformExpression } from './transforms/transformExpression'
|
||||
import { defaultOnError, createCompilerError, ErrorCodes } from './errors'
|
||||
import { transformStyle } from './transforms/transformStyle'
|
||||
|
||||
export type CompilerOptions = ParserOptions & TransformOptions & CodegenOptions
|
||||
|
||||
@@ -33,6 +34,7 @@ export function compile(
|
||||
transformIf,
|
||||
transformFor,
|
||||
...(prefixIdentifiers ? [transformExpression] : []),
|
||||
transformStyle,
|
||||
transformElement,
|
||||
...(options.nodeTransforms || []) // user transforms
|
||||
],
|
||||
@@ -53,7 +55,7 @@ export {
|
||||
createStructuralDirectiveTransform,
|
||||
TransformOptions,
|
||||
TransformContext,
|
||||
NodeTransform as Transform,
|
||||
NodeTransform,
|
||||
StructuralDirectiveTransform
|
||||
} from './transform'
|
||||
export {
|
||||
@@ -64,8 +66,3 @@ export {
|
||||
} from './codegen'
|
||||
export { ErrorCodes, CompilerError, createCompilerError } from './errors'
|
||||
export * from './ast'
|
||||
|
||||
// debug
|
||||
export {
|
||||
transformElement as prepareElementForCodegen
|
||||
} from './transforms/transformElement'
|
||||
|
||||
@@ -84,6 +84,7 @@ export function parse(content: string, options: ParserOptions = {}): RootNode {
|
||||
children: parseChildren(context, TextModes.DATA, []),
|
||||
imports: [],
|
||||
statements: [],
|
||||
hoists: [],
|
||||
loc: getSelection(context, start)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,7 +6,9 @@ import {
|
||||
ElementNode,
|
||||
DirectiveNode,
|
||||
Property,
|
||||
ExpressionNode
|
||||
ExpressionNode,
|
||||
createExpression,
|
||||
JSChildNode
|
||||
} from './ast'
|
||||
import { isString, isArray } from '@vue/shared'
|
||||
import { CompilerError, defaultOnError } from './errors'
|
||||
@@ -52,6 +54,7 @@ export interface TransformContext extends Required<TransformOptions> {
|
||||
root: RootNode
|
||||
imports: Set<string>
|
||||
statements: string[]
|
||||
hoists: JSChildNode[]
|
||||
identifiers: { [name: string]: number | undefined }
|
||||
parent: ParentNode
|
||||
childIndex: number
|
||||
@@ -61,6 +64,7 @@ export interface TransformContext extends Required<TransformOptions> {
|
||||
onNodeRemoved: () => void
|
||||
addIdentifier(exp: ExpressionNode): void
|
||||
removeIdentifier(exp: ExpressionNode): void
|
||||
hoist(exp: JSChildNode): ExpressionNode
|
||||
}
|
||||
|
||||
function createTransformContext(
|
||||
@@ -76,6 +80,7 @@ function createTransformContext(
|
||||
root,
|
||||
imports: new Set(),
|
||||
statements: [],
|
||||
hoists: [],
|
||||
identifiers: {},
|
||||
prefixIdentifiers,
|
||||
nodeTransforms,
|
||||
@@ -125,6 +130,14 @@ function createTransformContext(
|
||||
},
|
||||
removeIdentifier({ content }) {
|
||||
;(context.identifiers[content] as number)--
|
||||
},
|
||||
hoist(exp) {
|
||||
context.hoists.push(exp)
|
||||
return createExpression(
|
||||
`_hoisted_${context.hoists.length}`,
|
||||
false,
|
||||
exp.loc
|
||||
)
|
||||
}
|
||||
}
|
||||
return context
|
||||
@@ -135,6 +148,7 @@ export function transform(root: RootNode, options: TransformOptions) {
|
||||
traverseChildren(root, context)
|
||||
root.imports = [...context.imports]
|
||||
root.statements = context.statements
|
||||
root.hoists = context.hoists
|
||||
}
|
||||
|
||||
export function traverseChildren(
|
||||
|
||||
@@ -108,6 +108,7 @@ function buildProps(
|
||||
props: PropsExpression
|
||||
directives: DirectiveNode[]
|
||||
} {
|
||||
let isStatic = true
|
||||
let properties: ObjectExpression['properties'] = []
|
||||
const mergeArgs: PropsExpression[] = []
|
||||
const runtimeDirectives: DirectiveNode[] = []
|
||||
@@ -130,6 +131,7 @@ function buildProps(
|
||||
)
|
||||
} else {
|
||||
// directives
|
||||
isStatic = false
|
||||
const { name, arg, exp, loc } = prop
|
||||
// special case for v-bind and v-on with no argument
|
||||
const isBind = name === 'bind'
|
||||
@@ -208,6 +210,11 @@ function buildProps(
|
||||
)
|
||||
}
|
||||
|
||||
// hoist the object if it's fully static
|
||||
if (isStatic) {
|
||||
propsExpression = context.hoist(propsExpression)
|
||||
}
|
||||
|
||||
return {
|
||||
props: propsExpression,
|
||||
directives: runtimeDirectives
|
||||
@@ -233,10 +240,8 @@ function dedupeProperties(properties: Property[]): Property[] {
|
||||
const name = prop.key.content
|
||||
const existing = knownProps[name]
|
||||
if (existing) {
|
||||
if (name.startsWith('on')) {
|
||||
if (name.startsWith('on') || name === 'style') {
|
||||
mergeAsArray(existing, prop)
|
||||
} else if (name === 'style') {
|
||||
mergeStyles(existing, prop)
|
||||
} else if (name === 'class') {
|
||||
mergeClasses(existing, prop)
|
||||
}
|
||||
@@ -260,25 +265,9 @@ function mergeAsArray(existing: Property, incoming: Property) {
|
||||
}
|
||||
}
|
||||
|
||||
// Merge dynamic and static style into a single prop
|
||||
export function mergeStyles(existing: Property, incoming: Property) {
|
||||
if (
|
||||
existing.value.type === NodeTypes.JS_OBJECT_EXPRESSION &&
|
||||
incoming.value.type === NodeTypes.JS_OBJECT_EXPRESSION
|
||||
) {
|
||||
// if both are objects, merge the object expressions.
|
||||
// style="color: red" :style="{ a: b }"
|
||||
// -> { color: "red", a: b }
|
||||
existing.value.properties.push(...incoming.value.properties)
|
||||
} else {
|
||||
// otherwise merge as array
|
||||
// style="color:red" :style="a"
|
||||
// -> style: [{ color: "red" }, a]
|
||||
mergeAsArray(existing, incoming)
|
||||
}
|
||||
}
|
||||
|
||||
// Merge dynamic and static class into a single prop
|
||||
// :class="expression" class="string"
|
||||
// -> class: expression + "string"
|
||||
function mergeClasses(existing: Property, incoming: Property) {
|
||||
const e = existing.value as ExpressionNode
|
||||
const children =
|
||||
@@ -289,8 +278,6 @@ function mergeClasses(existing: Property, incoming: Property) {
|
||||
children: undefined
|
||||
}
|
||||
])
|
||||
// :class="expression" class="string"
|
||||
// -> class: expression + "string"
|
||||
children.push(` + " " + `, incoming.value as ExpressionNode)
|
||||
}
|
||||
|
||||
|
||||
@@ -28,12 +28,11 @@ export const transformExpression: NodeTransform = (node, context) => {
|
||||
if (prop.arg && !prop.arg.isStatic) {
|
||||
if (prop.name === 'class') {
|
||||
// TODO special expression optimization for classes
|
||||
processExpression(prop.arg, context)
|
||||
} else {
|
||||
processExpression(prop.arg, context)
|
||||
}
|
||||
}
|
||||
} else if (prop.name === 'style') {
|
||||
// TODO parse inline CSS literals into objects
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
37
packages/compiler-core/src/transforms/transformStyle.ts
Normal file
37
packages/compiler-core/src/transforms/transformStyle.ts
Normal file
@@ -0,0 +1,37 @@
|
||||
import { NodeTransform } from '../transform'
|
||||
import { NodeTypes, createExpression } from '../ast'
|
||||
|
||||
// prase inline CSS strings for static style attributes into an object
|
||||
export const transformStyle: NodeTransform = (node, context) => {
|
||||
if (node.type === NodeTypes.ELEMENT) {
|
||||
node.props.forEach((p, i) => {
|
||||
if (p.type === NodeTypes.ATTRIBUTE && p.name === 'style' && p.value) {
|
||||
// replace p with an expression node
|
||||
const parsed = JSON.stringify(parseInlineCSS(p.value.content))
|
||||
const exp = context.hoist(createExpression(parsed, false, p.loc))
|
||||
node.props[i] = {
|
||||
type: NodeTypes.DIRECTIVE,
|
||||
name: `bind`,
|
||||
arg: createExpression(`style`, true, p.loc),
|
||||
exp,
|
||||
modifiers: [],
|
||||
loc: p.loc
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const listDelimiterRE = /;(?![^(]*\))/g
|
||||
const propertyDelimiterRE = /:(.+)/
|
||||
|
||||
function parseInlineCSS(cssText: string): Record<string, string> {
|
||||
const res: Record<string, string> = {}
|
||||
cssText.split(listDelimiterRE).forEach(function(item) {
|
||||
if (item) {
|
||||
const tmp = item.split(propertyDelimiterRE)
|
||||
tmp.length > 1 && (res[tmp[0].trim()] = tmp[1].trim())
|
||||
}
|
||||
})
|
||||
return res
|
||||
}
|
||||
Reference in New Issue
Block a user