feat(compiler-vapor): node transform

This commit is contained in:
三咲智子 Kevin Deng 2023-12-01 07:34:18 +08:00
parent 0b765bcea3
commit cfd6d40d72
No known key found for this signature in database
GPG Key ID: 69992F2250DFD93E
14 changed files with 289 additions and 109 deletions

View File

@ -16,6 +16,9 @@ PR are welcome! However, please create an issue before you start to work on it,
- [x] simple bindings
- [x] simple events
- [ ] TODO-MVC App
- [ ] transform
- [x] NodeTransform
- [ ] DirectiveTransform
- [ ] directives
- [x] `v-once`
- [x] `v-html`

View File

@ -85,6 +85,13 @@ export interface Position {
column: number
}
export type AllNode =
| ParentNode
| ExpressionNode
| TemplateChildNode
| AttributeNode
| DirectiveNode
export type ParentNode = RootNode | ElementNode | IfBranchNode | ForNode
export type ExpressionNode = SimpleExpressionNode | CompoundExpressionNode

View File

@ -68,5 +68,6 @@ export { generateCodeFrame } from '@vue/shared'
export {
checkCompatEnabled,
warnDeprecation,
CompilerDeprecationTypes
CompilerDeprecationTypes,
type CompilerCompatOptions
} from './compat/compatConfig'

View File

@ -224,6 +224,7 @@ export function assert(condition: boolean, msg?: string) {
}
}
/** find directive */
export function findDir(
node: ElementNode,
name: string | RegExp,

View File

@ -104,16 +104,14 @@ export function render(_ctx) {
`;
exports[`compile > directives > v-once > as root node 1`] = `
"import { template, children, effect, setAttr } from 'vue/vapor';
"import { template, children, setAttr } from 'vue/vapor';
const t0 = template('<div></div>');
export function render(_ctx) {
const n0 = t0();
const {
0: [n1],
} = children(n0);
effect(() => {
setAttr(n1, 'id', undefined, foo);
});
setAttr(n1, 'id', undefined, foo);
return n0;
}
"

View File

@ -1,8 +1,12 @@
import { BindingTypes, CompilerOptions, RootNode } from '@vue/compiler-dom'
import { type RootNode, BindingTypes } from '@vue/compiler-dom'
import {
type CompilerOptions,
VaporErrorCodes,
compile as _compile,
} from '../src'
// TODO remove it
import { format } from 'prettier'
import { compile as _compile } from '../src'
import { ErrorCodes } from '../src/errors'
async function compile(
template: string | RootNode,
@ -78,7 +82,7 @@ describe('compile', () => {
await compile(`<div v-bind:arg />`, { onError })
expect(onError.mock.calls[0][0]).toMatchObject({
code: ErrorCodes.VAPOR_BIND_NO_EXPRESSION,
code: VaporErrorCodes.X_VAPOR_BIND_NO_EXPRESSION,
loc: {
start: {
line: 1,
@ -107,7 +111,7 @@ describe('compile', () => {
const onError = vi.fn()
await compile(`<div v-on:click />`, { onError })
expect(onError.mock.calls[0][0]).toMatchObject({
code: ErrorCodes.VAPOR_ON_NO_EXPRESSION,
code: VaporErrorCodes.X_VAPOR_ON_NO_EXPRESSION,
loc: {
start: {
line: 1,
@ -170,9 +174,9 @@ describe('compile', () => {
test('basic', async () => {
const code = await compile(
`<div v-once>
{{ msg }}
<span :class="clz" />
</div>`,
{{ msg }}
<span :class="clz" />
</div>`,
{
bindingMetadata: {
msg: BindingTypes.SETUP_REF,
@ -183,7 +187,7 @@ describe('compile', () => {
expect(code).matchSnapshot()
})
test.fails('as root node', async () => {
test('as root node', async () => {
const code = await compile(`<div :id="foo" v-once />`)
expect(code).toMatchSnapshot()
expect(code).not.contains('effect')

View File

@ -3,11 +3,11 @@ import { parse, compileScript } from '@vue/compiler-sfc'
import source from './fixtures/counter.vue?raw'
test('fixtures', async () => {
const { descriptor } = parse(source, { compiler: CompilerVapor })
const { descriptor } = parse(source, { compiler: CompilerVapor as any })
const script = compileScript(descriptor, {
id: 'counter.vue',
inlineTemplate: true,
templateOptions: { compiler: CompilerVapor },
templateOptions: { compiler: CompilerVapor as any },
})
expect(script.content).matchSnapshot()
})

View File

@ -1,19 +1,86 @@
import {
type CodegenResult,
type CompilerOptions,
type CompilerOptions as BaseCompilerOptions,
type RootNode,
type DirectiveTransform,
parse,
} from '@vue/compiler-dom'
import { isString } from '@vue/shared'
import { transform } from './transform'
import { extend, isString } from '@vue/shared'
import { NodeTransform, transform } from './transform'
import { generate } from './generate'
import { defaultOnError, createCompilerError, VaporErrorCodes } from './errors'
import { transformOnce } from './transforms/vOnce'
import { HackOptions } from './hack'
export type CompilerOptions = HackOptions<BaseCompilerOptions>
// TODO: copied from @vue/compiler-core
// code/AST -> IR -> JS codegen
export function compile(
template: string | RootNode,
source: string | RootNode,
options: CompilerOptions = {},
): CodegenResult {
const ast = isString(template) ? parse(template, options) : template
const ir = transform(ast, options)
return generate(ir, options)
const onError = options.onError || defaultOnError
const isModuleMode = options.mode === 'module'
/* istanbul ignore if */
if (__BROWSER__) {
if (options.prefixIdentifiers === true) {
onError(createCompilerError(VaporErrorCodes.X_PREFIX_ID_NOT_SUPPORTED))
} else if (isModuleMode) {
onError(createCompilerError(VaporErrorCodes.X_MODULE_MODE_NOT_SUPPORTED))
}
}
const prefixIdentifiers =
!__BROWSER__ && (options.prefixIdentifiers === true || isModuleMode)
// TODO scope id
// if (options.scopeId && !isModuleMode) {
// onError(createCompilerError(ErrorCodes.X_SCOPE_ID_NOT_SUPPORTED))
// }
const ast = isString(source) ? parse(source, options) : source
const [nodeTransforms, directiveTransforms] =
getBaseTransformPreset(prefixIdentifiers)
if (!__BROWSER__ && options.isTS) {
const { expressionPlugins } = options
if (!expressionPlugins || !expressionPlugins.includes('typescript')) {
options.expressionPlugins = [...(expressionPlugins || []), 'typescript']
}
}
const ir = transform(
ast,
extend({}, options, {
prefixIdentifiers,
nodeTransforms: [
...nodeTransforms,
...(options.nodeTransforms || []), // user transforms
],
directiveTransforms: extend(
{},
directiveTransforms,
options.directiveTransforms || {}, // user transforms
),
}),
)
return generate(
ir,
extend({}, options, {
prefixIdentifiers,
}),
)
}
export type TransformPreset = [
NodeTransform[],
Record<string, DirectiveTransform>,
]
export function getBaseTransformPreset(
prefixIdentifiers?: boolean,
): TransformPreset {
return [[transformOnce], {}]
}

View File

@ -1,7 +1,6 @@
import { CompilerError } from '@vue/compiler-dom'
import type { CompilerError } from '@vue/compiler-dom'
export { createCompilerError } from '@vue/compiler-dom'
export function defaultOnError(error: CompilerError) {
throw error
}
@ -10,14 +9,21 @@ export function defaultOnWarn(msg: CompilerError) {
__DEV__ && console.warn(`[Vue warn] ${msg.message}`)
}
export enum ErrorCodes {
export enum VaporErrorCodes {
// transform errors
VAPOR_BIND_NO_EXPRESSION,
VAPOR_ON_NO_EXPRESSION,
X_VAPOR_BIND_NO_EXPRESSION,
X_VAPOR_ON_NO_EXPRESSION,
// generic errors
X_PREFIX_ID_NOT_SUPPORTED,
X_MODULE_MODE_NOT_SUPPORTED,
}
export const errorMessages: Record<ErrorCodes, string> = {
export const errorMessages: Record<VaporErrorCodes, string> = {
// transform errors
[ErrorCodes.VAPOR_BIND_NO_EXPRESSION]: `v-bind is missing expression.`,
[ErrorCodes.VAPOR_ON_NO_EXPRESSION]: `v-on is missing expression.`,
[VaporErrorCodes.X_VAPOR_BIND_NO_EXPRESSION]: `v-bind is missing expression.`,
[VaporErrorCodes.X_VAPOR_ON_NO_EXPRESSION]: `v-on is missing expression.`,
[VaporErrorCodes.X_PREFIX_ID_NOT_SUPPORTED]: `"prefixIdentifiers" option is not supported in this build of compiler.`,
[VaporErrorCodes.X_MODULE_MODE_NOT_SUPPORTED]: `ES module mode is not supported in this build of compiler.`,
}

View File

@ -0,0 +1,9 @@
import type { Prettify } from '@vue/shared'
import type { NodeTransform } from './transform'
type Overwrite<T, U> = Pick<T, Exclude<keyof T, keyof U>> &
Pick<U, Extract<keyof U, keyof T>>
export type HackOptions<T> = Prettify<
Overwrite<T, { nodeTransforms?: NodeTransform[] }>
>

View File

@ -1,5 +1,6 @@
export { parse } from '@vue/compiler-dom'
export { transform } from './transform'
export { generate } from './generate'
export { compile } from './compile'
export { compile, type CompilerOptions } from './compile'
export * from './ir'
export * from './errors'

View File

@ -1,15 +1,17 @@
import {
type RootNode,
type Node,
type TemplateChildNode,
type ElementNode,
type AttributeNode,
type InterpolationNode,
type TransformOptions,
type TransformOptions as BaseTransformOptions,
type DirectiveNode,
type ExpressionNode,
type ParentNode,
type AllNode,
NodeTypes,
BindingTypes,
CompilerCompatOptions,
} from '@vue/compiler-dom'
import {
type OperationNode,
@ -17,25 +19,35 @@ import {
IRNodeTypes,
DynamicInfo,
} from './ir'
import { isVoidTag } from '@vue/shared'
import { EMPTY_OBJ, NOOP, isArray, isVoidTag } from '@vue/shared'
import {
ErrorCodes,
VaporErrorCodes,
createCompilerError,
defaultOnError,
defaultOnWarn,
} from './errors'
import { HackOptions } from './hack'
export interface TransformContext<T extends Node = Node> {
export type NodeTransform = (
node: RootNode | TemplateChildNode,
context: TransformContext,
) => void | (() => void) | (() => void)[]
export type TransformOptions = HackOptions<BaseTransformOptions>
export interface TransformContext<T extends AllNode = AllNode> {
node: T
parent: TransformContext | null
parent: TransformContext<ParentNode> | null
root: TransformContext<RootNode>
index: number
options: TransformOptions
options: Required<
Omit<TransformOptions, 'filename' | keyof CompilerCompatOptions>
>
template: string
dynamic: DynamicInfo
once: boolean
inVOnce: boolean
reference(): number
increaseId(): number
@ -45,10 +57,11 @@ export interface TransformContext<T extends Node = Node> {
helper(name: string): string
}
// TODO use class for better perf
function createRootContext(
ir: RootIRNode,
node: RootNode,
options: TransformOptions,
options: TransformOptions = {},
): TransformContext<RootNode> {
let globalId = 0
const { effect, operation: operation, helpers, vaporHelpers } = ir
@ -58,9 +71,32 @@ function createRootContext(
parent: null,
index: 0,
root: null!, // set later
options,
options: {
filename: '',
prefixIdentifiers: false,
hoistStatic: false,
hmr: false,
cacheHandlers: false,
nodeTransforms: [],
directiveTransforms: {},
transformHoist: null,
isBuiltInComponent: NOOP,
isCustomElement: NOOP,
expressionPlugins: [],
scopeId: null,
slotted: true,
ssr: false,
inSSR: false,
ssrCssVars: ``,
bindingMetadata: EMPTY_OBJ,
inline: false,
isTS: false,
onError: defaultOnError,
onWarn: defaultOnWarn,
...options,
},
dynamic: ir.dynamic,
once: false,
inVOnce: false,
increaseId: () => globalId++,
reference() {
@ -69,7 +105,7 @@ function createRootContext(
return (this.dynamic.id = this.increaseId())
},
registerEffect(expr, operation) {
if (this.once) {
if (this.inVOnce) {
return this.registerOperation(operation)
}
if (!effect[expr]) effect[expr] = []
@ -110,7 +146,7 @@ function createRootContext(
function createContext<T extends TemplateChildNode>(
node: T,
parent: TransformContext,
parent: TransformContext<ParentNode>,
index: number,
): TransformContext<T> {
const ctx: TransformContext<T> = {
@ -159,7 +195,7 @@ export function transform(
const ctx = createRootContext(ir, root, options)
// TODO: transform presets, see packages/compiler-core/src/transforms
transformChildren(ctx, true)
transformNode(ctx)
if (ir.template.length === 0) {
ir.template.push({
type: IRNodeTypes.FRAGMENT_FACTORY,
@ -170,20 +206,108 @@ export function transform(
return ir
}
function transformChildren(
ctx: TransformContext<RootNode | ElementNode>,
root?: boolean,
function transformNode(
context: TransformContext<RootNode | TemplateChildNode>,
) {
let { node, index } = context
// apply transform plugins
const { nodeTransforms } = context.options
const exitFns = []
for (const nodeTransform of nodeTransforms) {
// TODO nodeTransform type
const onExit = nodeTransform(node, context as any)
if (onExit) {
if (isArray(onExit)) {
exitFns.push(...onExit)
} else {
exitFns.push(onExit)
}
}
if (!context.node) {
// node was removed
return
} else {
// node may have been replaced
node = context.node
}
}
if (node.type === NodeTypes.ROOT) {
transformChildren(context as TransformContext<RootNode>)
return
}
const parentChildren = context.parent!.node.children
const isFirst = index === 0
const isLast = index === parentChildren.length - 1
switch (node.type) {
case NodeTypes.ELEMENT: {
transformElement(context as TransformContext<ElementNode>)
break
}
case NodeTypes.TEXT: {
context.template += node.content
break
}
case NodeTypes.COMMENT: {
context.template += `<!--${node.content}-->`
break
}
case NodeTypes.INTERPOLATION: {
transformInterpolation(
context as TransformContext<InterpolationNode>,
isFirst,
isLast,
)
break
}
case NodeTypes.TEXT_CALL:
// never
break
default: {
// TODO handle other types
// CompoundExpressionNode
// IfNode
// IfBranchNode
// ForNode
context.template += `[type: ${node.type}]`
}
}
// exit transforms
context.node = node
let i = exitFns.length
while (i--) {
exitFns[i]()
}
}
function transformChildren(ctx: TransformContext<RootNode | ElementNode>) {
const {
node: { children },
} = ctx
const childrenTemplate: string[] = []
children.forEach((child, i) => walkNode(child, i))
children.forEach((child, index) => {
const childContext = createContext(child, ctx, index)
transformNode(childContext)
childrenTemplate.push(childContext.template)
if (
childContext.dynamic.ghost ||
childContext.dynamic.referenced ||
childContext.dynamic.placeholder ||
Object.keys(childContext.dynamic.children).length
) {
ctx.dynamic.children[index] = childContext.dynamic
}
})
processDynamicChildren()
ctx.template += childrenTemplate.join('')
if (root) ctx.registerTemplate()
if (ctx.node.type === NodeTypes.ROOT) ctx.registerTemplate()
function processDynamicChildren() {
let prevChildren: DynamicInfo[] = []
@ -229,57 +353,6 @@ function transformChildren(
}
}
}
function walkNode(node: TemplateChildNode, index: number) {
const child = createContext(node, ctx, index)
const isFirst = index === 0
const isLast = index === children.length - 1
switch (node.type) {
case NodeTypes.ELEMENT: {
transformElement(child as TransformContext<ElementNode>)
break
}
case NodeTypes.TEXT: {
child.template += node.content
break
}
case NodeTypes.COMMENT: {
child.template += `<!--${node.content}-->`
break
}
case NodeTypes.INTERPOLATION: {
transformInterpolation(
child as TransformContext<InterpolationNode>,
isFirst,
isLast,
)
break
}
case NodeTypes.TEXT_CALL:
// never?
break
default: {
// TODO handle other types
// CompoundExpressionNode
// IfNode
// IfBranchNode
// ForNode
child.template += `[type: ${node.type}]`
}
}
childrenTemplate.push(child.template)
if (
child.dynamic.ghost ||
child.dynamic.referenced ||
child.dynamic.placeholder ||
Object.keys(child.dynamic.children).length
) {
ctx.dynamic.children[index] = child.dynamic
}
}
}
function transformElement(ctx: TransformContext<ElementNode>) {
@ -365,7 +438,7 @@ function transformProp(
(exp.type === NodeTypes.SIMPLE_EXPRESSION && !exp.content.trim())
) {
ctx.options.onError!(
createCompilerError(ErrorCodes.VAPOR_BIND_NO_EXPRESSION, loc),
createCompilerError(VaporErrorCodes.X_VAPOR_BIND_NO_EXPRESSION, loc),
)
return
}
@ -394,7 +467,7 @@ function transformProp(
case 'on': {
if (!exp && !modifiers.length) {
ctx.options.onError!(
createCompilerError(ErrorCodes.VAPOR_ON_NO_EXPRESSION, loc),
createCompilerError(VaporErrorCodes.X_VAPOR_ON_NO_EXPRESSION, loc),
)
return
}
@ -441,10 +514,6 @@ function transformProp(
})
break
}
case 'once': {
ctx.once = true
break
}
case 'cloak': {
// do nothing
break

View File

@ -0,0 +1,14 @@
import { NodeTypes, findDir } from '@vue/compiler-dom'
import { NodeTransform } from '../transform'
const seen = new WeakSet()
export const transformOnce: NodeTransform = (node, context) => {
if (node.type === NodeTypes.ELEMENT && findDir(node, 'once', true)) {
if (seen.has(node) || context.inVOnce /* || context.inSSR */) {
return
}
seen.add(node)
context.inVOnce = true
}
}

View File

@ -13,7 +13,7 @@ export default defineConfig({
plugins: [
Vue({
template: {
compiler: CompilerVapor
compiler: CompilerVapor as any
},
compiler: CompilerSFC
}),