refactor(compiler-vapor): CodeFragment for codegen

This commit is contained in:
三咲智子 Kevin Deng 2024-01-30 22:08:28 +08:00
parent d942be14f2
commit c0b7515369
No known key found for this signature in database
GPG Key ID: 69992F2250DFD93E
15 changed files with 522 additions and 444 deletions

View File

@ -17,7 +17,7 @@ export function render(_ctx) {
`; `;
exports[`compile > custom directive > basic 1`] = ` exports[`compile > custom directive > basic 1`] = `
"import { template as _template, children as _children, withDirectives as _withDirectives, resolveDirective("vTest") as _resolveDirective("vTest"), resolveDirective("vHello") as _resolveDirective("vHello") } from 'vue/vapor'; "import { template as _template, children as _children, resolveDirective("vTest") as _resolveDirective("vTest"), resolveDirective("vHello") as _resolveDirective("vHello"), withDirectives as _withDirectives } from 'vue/vapor';
export function render(_ctx) { export function render(_ctx) {
const t0 = _template("<div></div>") const t0 = _template("<div></div>")

View File

@ -1,7 +1,7 @@
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
exports[`compiler: v-if > basic v-if 1`] = ` exports[`compiler: v-if > basic v-if 1`] = `
"import { template as _template, fragment as _fragment, createIf as _createIf, children as _children, renderEffect as _renderEffect, setText as _setText, append as _append } from 'vue/vapor'; "import { template as _template, fragment as _fragment, children as _children, renderEffect as _renderEffect, setText as _setText, createIf as _createIf, append as _append } from 'vue/vapor';
export function render(_ctx) { export function render(_ctx) {
const t0 = _template("<div></div>") const t0 = _template("<div></div>")
@ -69,7 +69,7 @@ export function render(_ctx) {
`; `;
exports[`compiler: v-if > template v-if 1`] = ` exports[`compiler: v-if > template v-if 1`] = `
"import { template as _template, fragment as _fragment, createIf as _createIf, children as _children, renderEffect as _renderEffect, setText as _setText, append as _append } from 'vue/vapor'; "import { template as _template, fragment as _fragment, children as _children, renderEffect as _renderEffect, setText as _setText, createIf as _createIf, append as _append } from 'vue/vapor';
export function render(_ctx) { export function render(_ctx) {
const t0 = _template("<div></div>hello<p></p>") const t0 = _template("<div></div>hello<p></p>")

View File

@ -67,7 +67,7 @@ export function render(_ctx) {
`; `;
exports[`v-on > event modifier 1`] = ` exports[`v-on > event modifier 1`] = `
"import { template as _template, children as _children, on as _on, withModifiers as _withModifiers, withKeys as _withKeys } from 'vue/vapor'; "import { template as _template, children as _children, withModifiers as _withModifiers, on as _on, withKeys as _withKeys } from 'vue/vapor';
export function render(_ctx) { export function render(_ctx) {
const t0 = _template("<a></a><form></form><a></a><div></div><div></div><a></a><div></div><input><input><input><input><input><input><input><input><input><input><input><input><input><input><input>") const t0 = _template("<a></a><form></form><a></a><div></div><div></div><a></a><div></div><input><input><input><input><input><input><input><input><input><input><input><input><input><input><input>")
@ -239,7 +239,7 @@ export function render(_ctx) {
`; `;
exports[`v-on > should not wrap keys guard if no key modifier is present 1`] = ` exports[`v-on > should not wrap keys guard if no key modifier is present 1`] = `
"import { template as _template, children as _children, on as _on, withModifiers as _withModifiers } from 'vue/vapor'; "import { template as _template, children as _children, withModifiers as _withModifiers, on as _on } from 'vue/vapor';
export function render(_ctx) { export function render(_ctx) {
const t0 = _template("<div></div>") const t0 = _template("<div></div>")
@ -251,7 +251,7 @@ export function render(_ctx) {
`; `;
exports[`v-on > should support multiple events and modifiers options w/ prefixIdentifiers: true 1`] = ` exports[`v-on > should support multiple events and modifiers options w/ prefixIdentifiers: true 1`] = `
"import { template as _template, children as _children, on as _on, withModifiers as _withModifiers, withKeys as _withKeys } from 'vue/vapor'; "import { template as _template, children as _children, withModifiers as _withModifiers, on as _on, withKeys as _withKeys } from 'vue/vapor';
export function render(_ctx) { export function render(_ctx) {
const t0 = _template("<div></div>") const t0 = _template("<div></div>")
@ -264,7 +264,7 @@ export function render(_ctx) {
`; `;
exports[`v-on > should support multiple modifiers and event options w/ prefixIdentifiers: true 1`] = ` exports[`v-on > should support multiple modifiers and event options w/ prefixIdentifiers: true 1`] = `
"import { template as _template, children as _children, on as _on, withModifiers as _withModifiers } from 'vue/vapor'; "import { template as _template, children as _children, withModifiers as _withModifiers, on as _on } from 'vue/vapor';
export function render(_ctx) { export function render(_ctx) {
const t0 = _template("<div></div>") const t0 = _template("<div></div>")
@ -276,7 +276,7 @@ export function render(_ctx) {
`; `;
exports[`v-on > should transform click.middle 1`] = ` exports[`v-on > should transform click.middle 1`] = `
"import { template as _template, children as _children, on as _on, withModifiers as _withModifiers } from 'vue/vapor'; "import { template as _template, children as _children, withModifiers as _withModifiers, on as _on } from 'vue/vapor';
export function render(_ctx) { export function render(_ctx) {
const t0 = _template("<div></div>") const t0 = _template("<div></div>")
@ -288,7 +288,7 @@ export function render(_ctx) {
`; `;
exports[`v-on > should transform click.middle 2`] = ` exports[`v-on > should transform click.middle 2`] = `
"import { template as _template, children as _children, renderEffect as _renderEffect, on as _on, withModifiers as _withModifiers } from 'vue/vapor'; "import { template as _template, children as _children, renderEffect as _renderEffect, withModifiers as _withModifiers, on as _on } from 'vue/vapor';
export function render(_ctx) { export function render(_ctx) {
const t0 = _template("<div></div>") const t0 = _template("<div></div>")
@ -302,7 +302,7 @@ export function render(_ctx) {
`; `;
exports[`v-on > should transform click.right 1`] = ` exports[`v-on > should transform click.right 1`] = `
"import { template as _template, children as _children, on as _on, withModifiers as _withModifiers } from 'vue/vapor'; "import { template as _template, children as _children, withModifiers as _withModifiers, on as _on } from 'vue/vapor';
export function render(_ctx) { export function render(_ctx) {
const t0 = _template("<div></div>") const t0 = _template("<div></div>")
@ -314,7 +314,7 @@ export function render(_ctx) {
`; `;
exports[`v-on > should transform click.right 2`] = ` exports[`v-on > should transform click.right 2`] = `
"import { template as _template, children as _children, renderEffect as _renderEffect, on as _on, withKeys as _withKeys, withModifiers as _withModifiers } from 'vue/vapor'; "import { template as _template, children as _children, renderEffect as _renderEffect, withModifiers as _withModifiers, withKeys as _withKeys, on as _on } from 'vue/vapor';
export function render(_ctx) { export function render(_ctx) {
const t0 = _template("<div></div>") const t0 = _template("<div></div>")
@ -340,7 +340,7 @@ export function render(_ctx) {
`; `;
exports[`v-on > should wrap both for dynamic key event w/ left/right modifiers 1`] = ` exports[`v-on > should wrap both for dynamic key event w/ left/right modifiers 1`] = `
"import { template as _template, children as _children, renderEffect as _renderEffect, on as _on, withKeys as _withKeys, withModifiers as _withModifiers } from 'vue/vapor'; "import { template as _template, children as _children, renderEffect as _renderEffect, withModifiers as _withModifiers, withKeys as _withKeys, on as _on } from 'vue/vapor';
export function render(_ctx) { export function render(_ctx) {
const t0 = _template("<div></div>") const t0 = _template("<div></div>")
@ -354,7 +354,7 @@ export function render(_ctx) {
`; `;
exports[`v-on > should wrap keys guard for keyboard events or dynamic events 1`] = ` exports[`v-on > should wrap keys guard for keyboard events or dynamic events 1`] = `
"import { template as _template, children as _children, on as _on, withKeys as _withKeys, withModifiers as _withModifiers } from 'vue/vapor'; "import { template as _template, children as _children, withModifiers as _withModifiers, withKeys as _withKeys, on as _on } from 'vue/vapor';
export function render(_ctx) { export function render(_ctx) {
const t0 = _template("<div></div>") const t0 = _template("<div></div>")
@ -366,7 +366,7 @@ export function render(_ctx) {
`; `;
exports[`v-on > should wrap keys guard for static key event w/ left/right modifiers 1`] = ` exports[`v-on > should wrap keys guard for static key event w/ left/right modifiers 1`] = `
"import { template as _template, children as _children, on as _on, withKeys as _withKeys } from 'vue/vapor'; "import { template as _template, children as _children, withKeys as _withKeys, on as _on } from 'vue/vapor';
export function render(_ctx) { export function render(_ctx) {
const t0 = _template("<div></div>") const t0 = _template("<div></div>")

View File

@ -29,6 +29,7 @@ import { genSetModelValue } from './generators/modelValue'
import { genAppendNode, genInsertNode, genPrependNode } from './generators/dom' import { genAppendNode, genInsertNode, genPrependNode } from './generators/dom'
import { genWithDirective } from './generators/directive' import { genWithDirective } from './generators/directive'
import { genIf } from './generators/if' import { genIf } from './generators/if'
import { genTemplate } from './generators/template'
interface CodegenOptions extends BaseCodegenOptions { interface CodegenOptions extends BaseCodegenOptions {
expressionPlugins?: ParserPlugin[] expressionPlugins?: ParserPlugin[]
@ -38,35 +39,27 @@ interface CodegenOptions extends BaseCodegenOptions {
// @ts-expect-error // @ts-expect-error
function checkNever(x: never): never {} function checkNever(x: never): never {}
export type CodeFragment =
| string
| [code: string, newlineIndex?: number, loc?: SourceLocation, name?: string]
| undefined
export interface CodegenContext { export interface CodegenContext {
options: Required<CodegenOptions> options: Required<CodegenOptions>
source: string source: string
code: string code: CodeFragment[]
line: number
column: number
offset: number
indentLevel: number indentLevel: number
map?: SourceMapGenerator map?: SourceMapGenerator
push( push(...args: CodeFragment[]): void
code: string, newline(): CodeFragment
newlineIndex?: number, multi(
loc?: SourceLocation, codes: [left: string, right: string, segment: string],
name?: string, ...fn: Array<false | CodeFragment[]>
): void ): CodeFragment[]
newline( call(name: string, ...args: Array<false | CodeFragment[]>): CodeFragment[]
code?: string, withIndent<T>(fn: () => T): T
newlineIndex?: number,
loc?: SourceLocation,
name?: string,
): void
pushMulti(
codes: [left: string, right: string, segment?: string],
...fn: Array<false | string | (() => void)>
): void
pushCall(name: string, ...args: Array<false | string | (() => void)>): void
withIndent(fn: () => void): void
helpers: Set<string> helpers: Set<string>
vaporHelpers: Set<string> vaporHelpers: Set<string>
@ -77,6 +70,7 @@ export interface CodegenContext {
function createCodegenContext(ir: RootIRNode, options: CodegenOptions) { function createCodegenContext(ir: RootIRNode, options: CodegenOptions) {
const helpers = new Set<string>([]) const helpers = new Set<string>([])
const vaporHelpers = new Set<string>([]) const vaporHelpers = new Set<string>([])
const [code, push] = buildCodeFragment()
const context: CodegenContext = { const context: CodegenContext = {
options: extend( options: extend(
{ {
@ -100,10 +94,7 @@ function createCodegenContext(ir: RootIRNode, options: CodegenOptions) {
), ),
source: ir.source, source: ir.source,
code: '', code,
column: 1,
line: 1,
offset: 0,
indentLevel: 0, indentLevel: 0,
helpers, helpers,
@ -117,95 +108,35 @@ function createCodegenContext(ir: RootIRNode, options: CodegenOptions) {
return `_${name}` return `_${name}`
}, },
push(code, newlineIndex = NewlineType.None, loc, name) { push,
context.code += code newline() {
if (!__BROWSER__ && context.map) { return [`\n${` `.repeat(context.indentLevel)}`, NewlineType.Start]
if (loc) addMapping(loc.start, name)
if (newlineIndex === NewlineType.Unknown) {
// multiple newlines, full iteration
advancePositionWithMutation(context, code)
} else {
// fast paths
context.offset += code.length
if (newlineIndex === NewlineType.None) {
// no newlines; fast path to avoid newline detection
if (__TEST__ && code.includes('\n')) {
throw new Error(
`CodegenContext.push() called newlineIndex: none, but contains` +
`newlines: ${code.replace(/\n/g, '\\n')}`,
)
}
context.column += code.length
} else {
// single newline at known index
if (newlineIndex === NewlineType.End) {
newlineIndex = code.length - 1
}
if (
__TEST__ &&
(code.charAt(newlineIndex) !== '\n' ||
code.slice(0, newlineIndex).includes('\n') ||
code.slice(newlineIndex + 1).includes('\n'))
) {
throw new Error(
`CodegenContext.push() called with newlineIndex: ${newlineIndex} ` +
`but does not conform: ${code.replace(/\n/g, '\\n')}`,
)
}
context.line++
context.column = code.length - newlineIndex
}
}
if (loc && loc !== locStub) {
addMapping(loc.end)
}
}
}, },
newline(code, newlineIndex, node) { multi([left, right, seg], ...fns) {
context.push(`\n${` `.repeat(context.indentLevel)}`, NewlineType.Start) const frag: CodeFragment[] = []
code && context.push(code, newlineIndex, node)
},
pushMulti([left, right, seg], ...fns) {
fns = fns.filter(Boolean) fns = fns.filter(Boolean)
context.push(left) frag.push(left)
for (const [i, fn] of fns.entries()) { for (const [i, fn] of fns.entries()) {
if (isString(fn)) context.push(fn) if (fn) {
else (fn as () => void)() frag.push(...fn)
if (seg && i < fns.length - 1) context.push(seg) if (i < fns.length - 1) frag.push(seg)
} }
context.push(right) }
frag.push(right)
return frag
}, },
pushCall(name, ...args) { call(name, ...args) {
context.push(name) return [name, ...context.multi(['(', ')', ', '], ...args)]
context.pushMulti(['(', ')', ', '], ...args)
}, },
withIndent(fn) { withIndent(fn) {
++context.indentLevel ++context.indentLevel
fn() const ret = fn()
--context.indentLevel --context.indentLevel
return ret
}, },
} }
const filename = context.options.filename const filename = context.options.filename
function addMapping(loc: Position, name: string | null = null) {
// we use the private property to directly add the mapping
// because the addMapping() implementation in source-map-js has a bunch of
// unnecessary arg and validation checks that are pure overhead in our case.
const { _names, _mappings } = context.map!
if (name !== null && !_names.has(name)) _names.add(name)
_mappings.add({
originalLine: loc.line,
originalColumn: loc.column - 1, // source-map column is 0 based
generatedLine: context.line,
generatedColumn: context.column - 1,
source: filename,
// @ts-expect-error it is possible to be null
name,
})
}
if (!__BROWSER__ && context.options.sourceMap) { if (!__BROWSER__ && context.options.sourceMap) {
// lazy require source-map implementation, only in non-browser builds // lazy require source-map implementation, only in non-browser builds
context.map = new SourceMapGenerator() context.map = new SourceMapGenerator()
@ -228,37 +159,27 @@ export function generate(
options: CodegenOptions = {}, options: CodegenOptions = {},
): VaporCodegenResult { ): VaporCodegenResult {
const ctx = createCodegenContext(ir, options) const ctx = createCodegenContext(ir, options)
const { push, withIndent, newline, helpers, vaporHelper, vaporHelpers } = ctx const { push, withIndent, newline, helpers, vaporHelpers } = ctx
const functionName = 'render' const functionName = 'render'
const isSetupInlined = !!options.inline const isSetupInlined = !!options.inline
if (isSetupInlined) { if (isSetupInlined) {
push(`(() => {`) push(`(() => {`)
} else { } else {
push(
// placeholder for preamble // placeholder for preamble
newline() newline(),
newline(`export function ${functionName}(_ctx) {`) newline(),
`export function ${functionName}(_ctx) {`,
)
} }
withIndent(() => { withIndent(() => {
ir.template.forEach((template, i) => { ir.template.forEach((template, i) => push(...genTemplate(template, i, ctx)))
if (template.type === IRNodeTypes.TEMPLATE_FACTORY) { push(...genBlockFunctionContent(ir, ctx))
// TODO source map?
newline(
`const t${i} = ${vaporHelper('template')}(${JSON.stringify(
template.template,
)})`,
)
} else {
// fragment
newline(`const t${i} = ${vaporHelper('fragment')}()`)
}
}) })
genBlockFunctionContent(ir, ctx) push(newline())
})
newline()
if (isSetupInlined) { if (isSetupInlined) {
push('})()') push('})()')
} else { } else {
@ -276,12 +197,13 @@ export function generate(
.map(h => `${h} as _${h}`) .map(h => `${h} as _${h}`)
.join(', ')} } from 'vue';` .join(', ')} } from 'vue';`
let codegen = genCodeFragment(ctx)
if (!isSetupInlined) { if (!isSetupInlined) {
ctx.code = preamble + ctx.code codegen = preamble + codegen
} }
return { return {
code: ctx.code, code: codegen,
ast: ir, ast: ir,
preamble, preamble,
map: ctx.map ? ctx.map.toJSON() : undefined, map: ctx.map ? ctx.map.toJSON() : undefined,
@ -290,6 +212,82 @@ export function generate(
} }
} }
function genCodeFragment(context: CodegenContext) {
let codegen = ''
let line = 1
let column = 1
let offset = 0
for (let frag of context.code) {
if (!frag) continue
if (isString(frag)) frag = [frag]
let [code, newlineIndex = NewlineType.None, loc, name] = frag
codegen += code
if (!__BROWSER__ && context.map) {
if (loc) addMapping(loc.start, name)
if (newlineIndex === NewlineType.Unknown) {
// multiple newlines, full iteration
advancePositionWithMutation({ line, column, offset }, code)
} else {
// fast paths
offset += code.length
if (newlineIndex === NewlineType.None) {
// no newlines; fast path to avoid newline detection
if (__TEST__ && code.includes('\n')) {
throw new Error(
`CodegenContext.push() called newlineIndex: none, but contains` +
`newlines: ${code.replace(/\n/g, '\\n')}`,
)
}
column += code.length
} else {
// single newline at known index
if (newlineIndex === NewlineType.End) {
newlineIndex = code.length - 1
}
if (
__TEST__ &&
(code.charAt(newlineIndex) !== '\n' ||
code.slice(0, newlineIndex).includes('\n') ||
code.slice(newlineIndex + 1).includes('\n'))
) {
throw new Error(
`CodegenContext.push() called with newlineIndex: ${newlineIndex} ` +
`but does not conform: ${code.replace(/\n/g, '\\n')}`,
)
}
line++
column = code.length - newlineIndex
}
}
if (loc && loc !== locStub) {
addMapping(loc.end)
}
}
}
return codegen
function addMapping(loc: Position, name: string | null = null) {
// we use the private property to directly add the mapping
// because the addMapping() implementation in source-map-js has a bunch of
// unnecessary arg and validation checks that are pure overhead in our case.
const { _names, _mappings } = context.map!
if (name !== null && !_names.has(name)) _names.add(name)
_mappings.add({
originalLine: loc.line,
originalColumn: loc.column - 1, // source-map column is 0 based
generatedLine: line,
generatedColumn: column - 1,
source: context.options.filename,
// @ts-expect-error it is possible to be null
name,
})
}
}
function genChildren(children: IRDynamicInfo[]) { function genChildren(children: IRDynamicInfo[]) {
let code = '' let code = ''
let offset = 0 let offset = 0
@ -320,7 +318,10 @@ function genChildren(children: IRDynamicInfo[]) {
return `{${code}}` return `{${code}}`
} }
function genOperation(oper: OperationNode, context: CodegenContext) { function genOperation(
oper: OperationNode,
context: CodegenContext,
): CodeFragment[] {
// TODO: cache old value // TODO: cache old value
switch (oper.type) { switch (oper.type) {
case IRNodeTypes.SET_PROP: case IRNodeTypes.SET_PROP:
@ -347,22 +348,35 @@ function genOperation(oper: OperationNode, context: CodegenContext) {
return genIf(oper, context) return genIf(oper, context)
case IRNodeTypes.WITH_DIRECTIVE: case IRNodeTypes.WITH_DIRECTIVE:
// generated, skip // generated, skip
return break
default: default:
return checkNever(oper) return checkNever(oper)
} }
return []
}
export function buildCodeFragment() {
const frag: CodeFragment[] = []
const push = frag.push.bind(frag)
return [frag, push] as const
} }
export function genBlockFunctionContent( export function genBlockFunctionContent(
ir: BlockFunctionIRNode | RootIRNode, ir: BlockFunctionIRNode | RootIRNode,
ctx: CodegenContext, ctx: CodegenContext,
) { ): CodeFragment[] {
const { newline, withIndent, vaporHelper } = ctx const { newline, withIndent, vaporHelper } = ctx
newline(`const n${ir.dynamic.id} = t${ir.templateIndex}()`) const [frag, push] = buildCodeFragment()
push(newline(), `const n${ir.dynamic.id} = t${ir.templateIndex}()`)
const children = genChildren(ir.dynamic.children) const children = genChildren(ir.dynamic.children)
if (children) { if (children) {
newline(`const ${children} = ${vaporHelper('children')}(n${ir.dynamic.id})`) push(
newline(),
`const ${children} = ${vaporHelper('children')}(n${ir.dynamic.id})`,
)
} }
const directiveOps = ir.operation.filter( const directiveOps = ir.operation.filter(
@ -370,24 +384,24 @@ export function genBlockFunctionContent(
oper.type === IRNodeTypes.WITH_DIRECTIVE, oper.type === IRNodeTypes.WITH_DIRECTIVE,
) )
for (const directives of groupDirective(directiveOps)) { for (const directives of groupDirective(directiveOps)) {
genWithDirective(directives, ctx) push(...genWithDirective(directives, ctx))
} }
for (const operation of ir.operation) { for (const operation of ir.operation) {
genOperation(operation, ctx) push(...genOperation(operation, ctx))
} }
for (const { operations } of ir.effect) { for (const { operations } of ir.effect) {
newline(`${vaporHelper('renderEffect')}(() => {`) push(newline(), `${vaporHelper('renderEffect')}(() => {`)
withIndent(() => { withIndent(() => {
for (const operation of operations) { operations.forEach(op => push(...genOperation(op, ctx)))
genOperation(operation, ctx)
}
}) })
newline('})') push(newline(), '})')
} }
newline(`return n${ir.dynamic.id}`) push(newline(), `return n${ir.dynamic.id}`)
return frag
} }
function groupDirective(ops: WithDirectiveIRNode[]): WithDirectiveIRNode[][] { function groupDirective(ops: WithDirectiveIRNode[]): WithDirectiveIRNode[][] {

View File

@ -1,86 +1,73 @@
import { createSimpleExpression, isSimpleIdentifier } from '@vue/compiler-dom' import { createSimpleExpression, isSimpleIdentifier } from '@vue/compiler-dom'
import { camelize } from '@vue/shared' import { camelize } from '@vue/shared'
import { genExpression } from './expression' import { genExpression } from './expression'
import type { CodegenContext } from '../generate' import type { CodeFragment, CodegenContext } from '../generate'
import type { WithDirectiveIRNode } from '../ir' import type { WithDirectiveIRNode } from '../ir'
export function genWithDirective( export function genWithDirective(
opers: WithDirectiveIRNode[], opers: WithDirectiveIRNode[],
context: CodegenContext, context: CodegenContext,
) { ) {
const { newline, call, multi, vaporHelper } = context
const element = `n${opers[0].element}`
const directiveItems = opers.map(genDirective)
const directives = multi(['[', ']', ', '], ...directiveItems)
return [
newline(),
...call(vaporHelper('withDirectives'), [element], directives),
]
function genDirective({ dir, builtin }: WithDirectiveIRNode): CodeFragment[] {
const NULL = ['void 0']
const directive = genDirective()
const value = dir.exp
? ['() => ', ...genExpression(dir.exp, context)]
: dir.arg || dir.modifiers.length
? NULL
: false
const argument = dir.arg
? genExpression(dir.arg, context)
: dir.modifiers.length
? NULL
: false
const modifiers = dir.modifiers.length
? ['{ ', genDirectiveModifiers(), ' }']
: false
return multi(['[', ']', ', '], directive, value, argument, modifiers)
function genDirective(): CodeFragment[] {
const { const {
push,
newline,
pushCall,
pushMulti,
vaporHelper, vaporHelper,
options: { bindingMetadata }, options: { bindingMetadata },
} = context } = context
newline()
pushCall(
vaporHelper('withDirectives'),
// 1st arg: node
`n${opers[0].element}`,
// 2nd arg: directives
() => {
// directive
pushMulti(
['[', ']', ', '],
...opers.map(oper => () => {
push('[')
const { dir, builtin } = oper
if (dir.name === 'show') { if (dir.name === 'show') {
push(vaporHelper('vShow')) return [vaporHelper('vShow')]
} else if (builtin) { } else if (builtin) {
push(vaporHelper(builtin)) return [vaporHelper(builtin)]
} else { } else {
const directiveReference = camelize(`v-${dir.name}`) const directiveReference = camelize(`v-${dir.name}`)
// TODO resolve directive // TODO resolve directive
if (bindingMetadata[directiveReference]) { if (bindingMetadata[directiveReference]) {
const directiveExpression = const directiveExpression = createSimpleExpression(directiveReference)
createSimpleExpression(directiveReference)
directiveExpression.ast = null directiveExpression.ast = null
genExpression(directiveExpression, context) return genExpression(directiveExpression, context)
} else { } else {
push(vaporHelper(`resolveDirective("${directiveReference}")`)) return [vaporHelper(`resolveDirective("${directiveReference}")`)]
}
} }
} }
if (dir.exp) { function genDirectiveModifiers() {
push(', () => ') return dir.modifiers
genExpression(dir.exp, context)
} else if (dir.arg || dir.modifiers.length) {
push(', void 0')
}
if (dir.arg) {
push(', ')
genExpression(dir.arg, context)
} else if (dir.modifiers.length) {
push(', void 0')
}
if (dir.modifiers.length) {
push(', ')
push('{ ')
push(genDirectiveModifiers(dir.modifiers))
push(' }')
}
push(']')
}),
)
},
)
}
function genDirectiveModifiers(modifiers: string[]) {
return modifiers
.map( .map(
value => value =>
`${isSimpleIdentifier(value) ? value : JSON.stringify(value)}: true`, `${isSimpleIdentifier(value) ? value : JSON.stringify(value)}: true`,
) )
.join(', ') .join(', ')
}
}
} }

View File

@ -1,4 +1,4 @@
import type { CodegenContext } from '../generate' import type { CodeFragment, CodegenContext } from '../generate'
import type { import type {
AppendNodeIRNode, AppendNodeIRNode,
InsertNodeIRNode, InsertNodeIRNode,
@ -7,35 +7,47 @@ import type {
export function genInsertNode( export function genInsertNode(
oper: InsertNodeIRNode, oper: InsertNodeIRNode,
{ newline, pushCall, vaporHelper }: CodegenContext, { newline, call, vaporHelper }: CodegenContext,
) { ): CodeFragment[] {
const elements = ([] as number[]).concat(oper.element) const elements = ([] as number[]).concat(oper.element)
let element = elements.map(el => `n${el}`).join(', ') let element = elements.map(el => `n${el}`).join(', ')
if (elements.length > 1) element = `[${element}]` if (elements.length > 1) element = `[${element}]`
newline() return [
pushCall(vaporHelper('insert'), element, `n${oper.parent}`, `n${oper.anchor}`) newline(),
...call(
vaporHelper('insert'),
[element],
[`n${oper.parent}`],
[`n${oper.anchor}`],
),
]
} }
export function genPrependNode( export function genPrependNode(
oper: PrependNodeIRNode, oper: PrependNodeIRNode,
{ newline, pushCall, vaporHelper }: CodegenContext, { newline, call, vaporHelper }: CodegenContext,
) { ): CodeFragment[] {
newline() return [
pushCall( newline(),
...call(
vaporHelper('prepend'), vaporHelper('prepend'),
`n${oper.parent}`, [`n${oper.parent}`],
oper.elements.map(el => `n${el}`).join(', '), ...oper.elements.map(el => [`n${el}`]),
) ),
]
} }
export function genAppendNode( export function genAppendNode(
oper: AppendNodeIRNode, oper: AppendNodeIRNode,
{ newline, pushCall, vaporHelper }: CodegenContext, { newline, call, vaporHelper }: CodegenContext,
) { ): CodeFragment[] {
newline() newline()
pushCall( return [
newline(),
...call(
vaporHelper('append'), vaporHelper('append'),
`n${oper.parent}`, [`n${oper.parent}`],
oper.elements.map(el => `n${el}`).join(', '), ...oper.elements.map(el => [`n${el}`]),
) ),
]
} }

View File

@ -1,5 +1,5 @@
import { isMemberExpression } from '@vue/compiler-dom' import { isMemberExpression } from '@vue/compiler-dom'
import type { CodegenContext } from '../generate' import type { CodeFragment, CodegenContext } from '../generate'
import type { SetEventIRNode } from '../ir' import type { SetEventIRNode } from '../ir'
import { genExpression } from './expression' import { genExpression } from './expression'
@ -7,60 +7,38 @@ import { genExpression } from './expression'
const fnExpRE = const fnExpRE =
/^\s*([\w$_]+|(async\s*)?\([^)]*?\))\s*(:[^=]+)?=>|^\s*(async\s+)?function(?:\s+[\w$]+)?\s*\(/ /^\s*([\w$_]+|(async\s*)?\([^)]*?\))\s*(:[^=]+)?=>|^\s*(async\s+)?function(?:\s+[\w$]+)?\s*\(/
export function genSetEvent(oper: SetEventIRNode, context: CodegenContext) { export function genSetEvent(
const { oper: SetEventIRNode,
vaporHelper, context: CodegenContext,
push, ): CodeFragment[] {
newline, const { vaporHelper, newline, call, options: ctxOptions } = context
pushMulti,
pushCall,
options: ctxOptions,
} = context
const { keys, nonKeys, options } = oper.modifiers const { keys, nonKeys, options } = oper.modifiers
newline() const name = genName()
pushCall( const handler = genFinalizedHandler()
vaporHelper('on'), const opt = !!options.length && [
// 1st arg: event name `{ ${options.map(v => `${v}: true`).join(', ')} }`,
() => push(`n${oper.element}`), ]
// 2nd arg: event name
() => { return [
newline(),
...call(vaporHelper('on'), [`n${oper.element}`], name, handler, opt),
]
function genName(): CodeFragment[] {
const expr = genExpression(oper.key, context)
// TODO unit test
if (oper.keyOverride) { if (oper.keyOverride) {
const find = JSON.stringify(oper.keyOverride[0]) const find = JSON.stringify(oper.keyOverride[0])
const replacement = JSON.stringify(oper.keyOverride[1]) const replacement = JSON.stringify(oper.keyOverride[1])
pushMulti(['(', ')'], () => genExpression(oper.key, context)) const wrapped: CodeFragment[] = ['(', ...expr, ')']
push(` === ${find} ? ${replacement} : `) return [...wrapped, ` === ${find} ? ${replacement} : `, ...wrapped]
pushMulti(['(', ')'], () => genExpression(oper.key, context))
} else { } else {
genExpression(oper.key, context) return genExpression(oper.key, context)
} }
},
// 3rd arg: event handler
() => {
const pushWithKeys = (fn: () => void) => {
push(`${vaporHelper('withKeys')}(`)
fn()
push(`, ${genArrayExpression(keys)})`)
} }
const pushWithModifiers = (fn: () => void) => {
push(`${vaporHelper('withModifiers')}(`)
fn()
push(`, ${genArrayExpression(nonKeys)})`)
}
const pushNoop = (fn: () => void) => fn()
;(keys.length ? pushWithKeys : pushNoop)(() => function genEventHandler(): CodeFragment[] {
(nonKeys.length ? pushWithModifiers : pushNoop)(() => {
genEventHandler()
}),
)
},
// 4th arg, gen options
!!options.length &&
(() => push(`{ ${options.map(v => `${v}: true`).join(', ')} }`)),
)
function genEventHandler() {
const exp = oper.value const exp = oper.value
if (exp && exp.content.trim()) { if (exp && exp.content.trim()) {
const isMemberExp = isMemberExpression(exp.content, ctxOptions) const isMemberExp = isMemberExpression(exp.content, ctxOptions)
@ -68,25 +46,53 @@ export function genSetEvent(oper: SetEventIRNode, context: CodegenContext) {
const hasMultipleStatements = exp.content.includes(`;`) const hasMultipleStatements = exp.content.includes(`;`)
if (isInlineStatement) { if (isInlineStatement) {
push('$event => ')
push(hasMultipleStatements ? '{' : '(')
const knownIds = Object.create(null) const knownIds = Object.create(null)
knownIds['$event'] = 1 knownIds['$event'] = 1
genExpression(exp, context, knownIds)
push(hasMultipleStatements ? '}' : ')') return [
} else if (isMemberExp) { '$event => ',
push('(...args) => (') hasMultipleStatements ? '{' : '(',
genExpression(exp, context) ...genExpression(exp, context, knownIds),
push(' && ') hasMultipleStatements ? '}' : ')',
genExpression(exp, context) ]
push('(...args))')
} else { } else {
genExpression(exp, context) const expr = genExpression(exp, context)
} if (isMemberExp) {
return ['(...args) => (', ...expr, ' && ', ...expr, '(...args))']
} else { } else {
push('() => {}') return expr
} }
} }
}
return ['() => {}']
}
function genFinalizedHandler(): CodeFragment[] {
let expr = genEventHandler()
if (nonKeys.length) {
expr = [
vaporHelper('withModifiers'),
'(',
...expr,
', ',
genArrayExpression(nonKeys),
')',
]
}
if (keys.length) {
expr = [
vaporHelper('withKeys'),
'(',
...expr,
', ',
genArrayExpression(keys),
')',
]
}
return expr
}
} }
function genArrayExpression(elements: string[]) { function genArrayExpression(elements: string[]) {

View File

@ -8,23 +8,27 @@ import {
import { isGloballyAllowed, isString, makeMap } from '@vue/shared' import { isGloballyAllowed, isString, makeMap } from '@vue/shared'
import type { Identifier } from '@babel/types' import type { Identifier } from '@babel/types'
import type { IRExpression } from '../ir' import type { IRExpression } from '../ir'
import type { CodegenContext } from '../generate' import {
type CodeFragment,
type CodegenContext,
buildCodeFragment,
} from '../generate'
export function genExpression( export function genExpression(
node: IRExpression, node: IRExpression,
context: CodegenContext, context: CodegenContext,
knownIds: Record<string, number> = Object.create(null), knownIds: Record<string, number> = Object.create(null),
): void { ): CodeFragment[] {
const { const {
push,
options: { prefixIdentifiers }, options: { prefixIdentifiers },
} = context } = context
if (isString(node)) return push(node) if (isString(node)) return [node]
const { content: rawExpr, ast, isStatic, loc } = node const { content: rawExpr, ast, isStatic, loc } = node
if (isStatic) { if (isStatic) {
return push(JSON.stringify(rawExpr), NewlineType.None, loc) return [[JSON.stringify(rawExpr), NewlineType.None, loc]]
} }
if ( if (
__BROWSER__ || __BROWSER__ ||
!prefixIdentifiers || !prefixIdentifiers ||
@ -34,12 +38,12 @@ export function genExpression(
isGloballyAllowed(rawExpr) || isGloballyAllowed(rawExpr) ||
isLiteralWhitelisted(rawExpr) isLiteralWhitelisted(rawExpr)
) { ) {
return push(rawExpr, NewlineType.None, loc) return [[rawExpr, NewlineType.None, loc]]
} }
if (ast === null) { if (ast === null) {
// the expression is a simple identifier // the expression is a simple identifier
return genIdentifier(rawExpr, context, loc) return [genIdentifier(rawExpr, context, loc)]
} }
const ids: Identifier[] = [] const ids: Identifier[] = []
@ -55,6 +59,7 @@ export function genExpression(
) )
if (ids.length) { if (ids.length) {
ids.sort((a, b) => a.start! - b.start!) ids.sort((a, b) => a.start! - b.start!)
const [frag, push] = buildCodeFragment()
ids.forEach((id, i) => { ids.forEach((id, i) => {
// range is offset by -1 due to the wrapping parens when parsed // range is offset by -1 due to the wrapping parens when parsed
const start = id.start! - 1 const start = id.start! - 1
@ -62,21 +67,24 @@ export function genExpression(
const last = ids[i - 1] const last = ids[i - 1]
const leadingText = rawExpr.slice(last ? last.end! - 1 : 0, start) const leadingText = rawExpr.slice(last ? last.end! - 1 : 0, start)
if (leadingText.length) push(leadingText, NewlineType.Unknown) if (leadingText.length) push([leadingText, NewlineType.Unknown])
const source = rawExpr.slice(start, end) const source = rawExpr.slice(start, end)
push(
genIdentifier(source, context, { genIdentifier(source, context, {
start: advancePositionWithClone(node.loc.start, source, start), start: advancePositionWithClone(node.loc.start, source, start),
end: advancePositionWithClone(node.loc.start, source, end), end: advancePositionWithClone(node.loc.start, source, end),
source, source,
}) }),
)
if (i === ids.length - 1 && end < rawExpr.length) { if (i === ids.length - 1 && end < rawExpr.length) {
push(rawExpr.slice(end), NewlineType.Unknown) push([rawExpr.slice(end), NewlineType.Unknown])
} }
}) })
return frag
} else { } else {
push(rawExpr, NewlineType.Unknown) return [[rawExpr, NewlineType.Unknown]]
} }
} }
@ -84,9 +92,9 @@ const isLiteralWhitelisted = /*#__PURE__*/ makeMap('true,false,null,this')
function genIdentifier( function genIdentifier(
id: string, id: string,
{ options, vaporHelper, push }: CodegenContext, { options, vaporHelper }: CodegenContext,
loc?: SourceLocation, loc?: SourceLocation,
): void { ): CodeFragment {
const { inline, bindingMetadata } = options const { inline, bindingMetadata } = options
let name: string | undefined = id let name: string | undefined = id
if (inline) { if (inline) {
@ -102,5 +110,5 @@ function genIdentifier(
} else { } else {
id = `_ctx.${id}` id = `_ctx.${id}`
} }
push(id, NewlineType.None, loc, name) return [id, NewlineType.None, loc, name]
} }

View File

@ -1,11 +1,18 @@
import type { CodegenContext } from '../generate' import type { CodeFragment, CodegenContext } from '../generate'
import type { SetHtmlIRNode } from '../ir' import type { SetHtmlIRNode } from '../ir'
import { genExpression } from './expression' import { genExpression } from './expression'
export function genSetHtml(oper: SetHtmlIRNode, context: CodegenContext) { export function genSetHtml(
const { newline, pushCall, vaporHelper } = context oper: SetHtmlIRNode,
newline() context: CodegenContext,
pushCall(vaporHelper('setHtml'), `n${oper.element}`, () => ): CodeFragment[] {
const { newline, call, vaporHelper } = context
return [
newline(),
...call(
vaporHelper('setHtml'),
[`n${oper.element}`],
genExpression(oper.value, context), genExpression(oper.value, context),
) ),
]
} }

View File

@ -1,4 +1,9 @@
import { type CodegenContext, genBlockFunctionContent } from '../generate' import {
type CodeFragment,
type CodegenContext,
buildCodeFragment,
genBlockFunctionContent,
} from '../generate'
import { type BlockFunctionIRNode, IRNodeTypes, type IfIRNode } from '../ir' import { type BlockFunctionIRNode, IRNodeTypes, type IfIRNode } from '../ir'
import { genExpression } from './expression' import { genExpression } from './expression'
@ -6,43 +11,45 @@ export function genIf(
oper: IfIRNode, oper: IfIRNode,
context: CodegenContext, context: CodegenContext,
isNested = false, isNested = false,
) { ): CodeFragment[] {
const { pushCall, vaporHelper, newline, push } = context const { call, vaporHelper, newline } = context
const { condition, positive, negative } = oper const { condition, positive, negative } = oper
const [frag, push] = buildCodeFragment()
let positiveArg = () => genBlockFunction(positive, context) const conditionExpr: CodeFragment[] = [
let negativeArg: false | (() => void) = false '() => (',
...genExpression(condition, context),
')',
]
let positiveArg = genBlockFunction(positive, context)
let negativeArg: false | CodeFragment[] = false
if (negative) { if (negative) {
if (negative.type === IRNodeTypes.BLOCK_FUNCTION) { if (negative.type === IRNodeTypes.BLOCK_FUNCTION) {
negativeArg = () => genBlockFunction(negative, context) negativeArg = genBlockFunction(negative, context)
} else { } else {
negativeArg = () => { negativeArg = ['() => ', ...genIf(negative!, context, true)]
push('() => ')
genIf(negative!, context, true)
}
} }
} }
if (!isNested) newline(`const n${oper.id} = `) if (!isNested) push(newline(), `const n${oper.id} = `)
pushCall( push(
vaporHelper('createIf'), ...call(vaporHelper('createIf'), conditionExpr, positiveArg, negativeArg),
() => {
push('() => (')
genExpression(condition, context)
push(')')
},
positiveArg,
negativeArg,
) )
return frag
} }
function genBlockFunction(oper: BlockFunctionIRNode, context: CodegenContext) { function genBlockFunction(
const { newline, push, withIndent } = context oper: BlockFunctionIRNode,
context: CodegenContext,
push('() => {') ): CodeFragment[] {
withIndent(() => { const { newline, withIndent } = context
genBlockFunctionContent(oper, context) return [
}) '() => {',
newline('}') ...withIndent(() => genBlockFunctionContent(oper, context)),
newline(),
'}',
]
} }

View File

@ -1,41 +1,41 @@
import { camelize, isString } from '@vue/shared' import { camelize, isString } from '@vue/shared'
import { genExpression } from './expression' import { genExpression } from './expression'
import type { SetModelValueIRNode } from '../ir' import type { SetModelValueIRNode } from '../ir'
import type { CodegenContext } from '../generate' import type { CodeFragment, CodegenContext } from '../generate'
export function genSetModelValue( export function genSetModelValue(
oper: SetModelValueIRNode, oper: SetModelValueIRNode,
context: CodegenContext, context: CodegenContext,
) { ): CodeFragment[] {
const { const {
vaporHelper, vaporHelper,
push,
newline, newline,
pushCall, call,
options: { isTS }, options: { isTS },
} = context } = context
newline() const name = genName()
pushCall( const handler = genHandler()
vaporHelper('on'),
// 1st arg: event name return [
() => push(`n${oper.element}`), newline(),
// 2nd arg: event name ...call(vaporHelper('on'), [`n${oper.element}`], name, handler),
() => { ]
function genName(): CodeFragment[] {
if (isString(oper.key)) { if (isString(oper.key)) {
push(JSON.stringify(`update:${camelize(oper.key)}`)) return [JSON.stringify(`update:${camelize(oper.key)}`)]
} else { } else {
push('`update:${') return ['`update:${', ...genExpression(oper.key, context), '}`']
genExpression(oper.key, context)
push('}`')
} }
}, }
// 3rd arg: event handler
() => { function genHandler(): CodeFragment[] {
push((isTS ? `($event: any)` : `$event`) + ' => ((') return [
(isTS ? `($event: any)` : `$event`) + ' => ((',
// TODO handle not a ref // TODO handle not a ref
genExpression(oper.value, context) ...genExpression(oper.value, context),
push(') = $event)') ') = $event)',
}, ]
) }
} }

View File

@ -1,14 +1,16 @@
import type { CodegenContext } from '../generate' import type { CodeFragment, CodegenContext } from '../generate'
import type { SetPropIRNode } from '../ir' import type { SetPropIRNode } from '../ir'
import { genExpression } from './expression' import { genExpression } from './expression'
import { isString } from '@vue/shared' import { isString } from '@vue/shared'
export function genSetProp(oper: SetPropIRNode, context: CodegenContext) { export function genSetProp(
const { pushCall, pushMulti, newline, vaporHelper, helper } = context oper: SetPropIRNode,
context: CodegenContext,
): CodeFragment[] {
const { call, newline, vaporHelper, helper } = context
newline() const element = [`n${oper.element}`]
const expr = genExpression(oper.key, context)
const element = `n${oper.element}`
// fast path for static props // fast path for static props
if (isString(oper.key) || oper.key.isStatic) { if (isString(oper.key) || oper.key.isStatic) {
@ -27,40 +29,35 @@ export function genSetProp(oper: SetPropIRNode, context: CodegenContext) {
} }
if (helperName) { if (helperName) {
pushCall( return [
newline(),
...call(
vaporHelper(helperName), vaporHelper(helperName),
element, element,
omitKey omitKey ? false : expr,
? false genExpression(oper.value, context),
: () => { ),
const expr = () => genExpression(oper.key, context) ]
if (oper.runtimeCamelize) {
pushCall(helper('camelize'), expr)
} else {
expr()
}
},
() => genExpression(oper.value, context),
)
return
} }
} }
pushCall( return [
newline(),
...call(
vaporHelper('setDynamicProp'), vaporHelper('setDynamicProp'),
element, element,
// 2. key name genDynamicKey(),
() => { genExpression(oper.value, context),
),
]
function genDynamicKey(): CodeFragment[] {
if (oper.runtimeCamelize) { if (oper.runtimeCamelize) {
pushCall(helper('camelize'), () => genExpression(oper.key, context)) return call(helper('camelize'), expr)
} else if (oper.modifier) { } else if (oper.modifier) {
pushMulti([`\`${oper.modifier}\${`, `}\``], () => return [`\`${oper.modifier}\${`, ...expr, `}\``]
genExpression(oper.key, context),
)
} else { } else {
genExpression(oper.key, context) return expr
}
} }
},
() => genExpression(oper.value, context),
)
} }

View File

@ -1,11 +1,18 @@
import type { CodegenContext } from '../generate'
import type { SetRefIRNode } from '../ir'
import { genExpression } from './expression' import { genExpression } from './expression'
import type { CodeFragment, CodegenContext } from '../generate'
import type { SetRefIRNode } from '../ir'
export function genSetRef(oper: SetRefIRNode, context: CodegenContext) { export function genSetRef(
const { newline, pushCall, vaporHelper } = context oper: SetRefIRNode,
newline() context: CodegenContext,
pushCall(vaporHelper('setRef'), `n${oper.element}`, () => ): CodeFragment[] {
const { newline, call, vaporHelper } = context
return [
newline(),
...call(
vaporHelper('setRef'),
[`n${oper.element}`],
genExpression(oper.value, context), genExpression(oper.value, context),
) ),
]
} }

View File

@ -0,0 +1,25 @@
import type { CodeFragment, CodegenContext } from '../generate'
import {
type FragmentFactoryIRNode,
IRNodeTypes,
type TemplateFactoryIRNode,
} from '../ir'
export function genTemplate(
node: TemplateFactoryIRNode | FragmentFactoryIRNode,
index: number,
{ newline, vaporHelper }: CodegenContext,
): CodeFragment[] {
if (node.type === IRNodeTypes.TEMPLATE_FACTORY) {
// TODO source map?
return [
newline(),
`const t${index} = ${vaporHelper('template')}(${JSON.stringify(
node.template,
)})`,
]
} else {
// fragment
return [newline(), `const t${index} = ${vaporHelper('fragment')}()`]
}
}

View File

@ -1,22 +1,30 @@
import type { CodegenContext } from '../generate' import type { CodeFragment, CodegenContext } from '../generate'
import type { CreateTextNodeIRNode, SetTextIRNode } from '../ir' import type { CreateTextNodeIRNode, SetTextIRNode } from '../ir'
import { genExpression } from './expression' import { genExpression } from './expression'
export function genSetText(oper: SetTextIRNode, context: CodegenContext) { export function genSetText(
const { pushCall, newline, vaporHelper } = context oper: SetTextIRNode,
newline() context: CodegenContext,
pushCall(vaporHelper('setText'), `n${oper.element}`, () => ): CodeFragment[] {
const { call, newline, vaporHelper } = context
return [
newline(),
...call(
vaporHelper('setText'),
[`n${oper.element}`],
genExpression(oper.value, context), genExpression(oper.value, context),
) ),
]
} }
export function genCreateTextNode( export function genCreateTextNode(
oper: CreateTextNodeIRNode, oper: CreateTextNodeIRNode,
context: CodegenContext, context: CodegenContext,
) { ): CodeFragment[] {
const { newline, pushCall, vaporHelper } = context const { newline, call, vaporHelper } = context
newline(`const n${oper.id} = `) return [
pushCall(vaporHelper('createTextNode'), () => newline(),
genExpression(oper.value, context), `const n${oper.id} = `,
) ...call(vaporHelper('createTextNode'), genExpression(oper.value, context)),
]
} }