From: 三咲智子 Kevin Deng Date: Tue, 30 Jan 2024 14:08:28 +0000 (+0800) Subject: refactor(compiler-vapor): CodeFragment for codegen X-Git-Tag: v3.6.0-alpha.1~16^2~631 X-Git-Url: http://git.ipfire.org/cgi-bin/gitweb.cgi?a=commitdiff_plain;h=c0b7515369fec6f3dc53168a1b32851421ffc16f;p=thirdparty%2Fvuejs%2Fcore.git refactor(compiler-vapor): CodeFragment for codegen --- diff --git a/packages/compiler-vapor/__tests__/__snapshots__/compile.spec.ts.snap b/packages/compiler-vapor/__tests__/__snapshots__/compile.spec.ts.snap index 35bc588ce1..dfde624753 100644 --- a/packages/compiler-vapor/__tests__/__snapshots__/compile.spec.ts.snap +++ b/packages/compiler-vapor/__tests__/__snapshots__/compile.spec.ts.snap @@ -17,7 +17,7 @@ export function render(_ctx) { `; 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) { const t0 = _template("
") diff --git a/packages/compiler-vapor/__tests__/transforms/__snapshots__/vIf.spec.ts.snap b/packages/compiler-vapor/__tests__/transforms/__snapshots__/vIf.spec.ts.snap index bc726ca126..791e2c0852 100644 --- a/packages/compiler-vapor/__tests__/transforms/__snapshots__/vIf.spec.ts.snap +++ b/packages/compiler-vapor/__tests__/transforms/__snapshots__/vIf.spec.ts.snap @@ -1,7 +1,7 @@ // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html 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) { const t0 = _template("
") @@ -69,7 +69,7 @@ export function render(_ctx) { `; 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) { const t0 = _template("
hello

") diff --git a/packages/compiler-vapor/__tests__/transforms/__snapshots__/vOn.spec.ts.snap b/packages/compiler-vapor/__tests__/transforms/__snapshots__/vOn.spec.ts.snap index a469d61bd4..ba5cd77b01 100644 --- a/packages/compiler-vapor/__tests__/transforms/__snapshots__/vOn.spec.ts.snap +++ b/packages/compiler-vapor/__tests__/transforms/__snapshots__/vOn.spec.ts.snap @@ -67,7 +67,7 @@ export function render(_ctx) { `; 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) { const t0 = _template("
") @@ -239,7 +239,7 @@ export function render(_ctx) { `; 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) { const t0 = _template("
") @@ -251,7 +251,7 @@ export function render(_ctx) { `; 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) { const t0 = _template("
") @@ -264,7 +264,7 @@ export function render(_ctx) { `; 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) { const t0 = _template("
") @@ -276,7 +276,7 @@ export function render(_ctx) { `; 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) { const t0 = _template("
") @@ -288,7 +288,7 @@ export function render(_ctx) { `; 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) { const t0 = _template("
") @@ -302,7 +302,7 @@ export function render(_ctx) { `; 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) { const t0 = _template("
") @@ -314,7 +314,7 @@ export function render(_ctx) { `; 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) { const t0 = _template("
") @@ -340,7 +340,7 @@ export function render(_ctx) { `; 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) { const t0 = _template("
") @@ -354,7 +354,7 @@ export function render(_ctx) { `; 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) { const t0 = _template("
") @@ -366,7 +366,7 @@ export function render(_ctx) { `; 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) { const t0 = _template("
") diff --git a/packages/compiler-vapor/src/generate.ts b/packages/compiler-vapor/src/generate.ts index 240cc73474..b24e231a25 100644 --- a/packages/compiler-vapor/src/generate.ts +++ b/packages/compiler-vapor/src/generate.ts @@ -29,6 +29,7 @@ import { genSetModelValue } from './generators/modelValue' import { genAppendNode, genInsertNode, genPrependNode } from './generators/dom' import { genWithDirective } from './generators/directive' import { genIf } from './generators/if' +import { genTemplate } from './generators/template' interface CodegenOptions extends BaseCodegenOptions { expressionPlugins?: ParserPlugin[] @@ -38,35 +39,27 @@ interface CodegenOptions extends BaseCodegenOptions { // @ts-expect-error function checkNever(x: never): never {} +export type CodeFragment = + | string + | [code: string, newlineIndex?: number, loc?: SourceLocation, name?: string] + | undefined + export interface CodegenContext { options: Required source: string - code: string - line: number - column: number - offset: number + code: CodeFragment[] indentLevel: number map?: SourceMapGenerator - push( - code: string, - newlineIndex?: number, - loc?: SourceLocation, - name?: string, - ): void - newline( - code?: string, - newlineIndex?: number, - loc?: SourceLocation, - name?: string, - ): void - pushMulti( - codes: [left: string, right: string, segment?: string], - ...fn: Array void)> - ): void - pushCall(name: string, ...args: Array void)>): void - withIndent(fn: () => void): void + push(...args: CodeFragment[]): void + newline(): CodeFragment + multi( + codes: [left: string, right: string, segment: string], + ...fn: Array + ): CodeFragment[] + call(name: string, ...args: Array): CodeFragment[] + withIndent(fn: () => T): T helpers: Set vaporHelpers: Set @@ -77,6 +70,7 @@ export interface CodegenContext { function createCodegenContext(ir: RootIRNode, options: CodegenOptions) { const helpers = new Set([]) const vaporHelpers = new Set([]) + const [code, push] = buildCodeFragment() const context: CodegenContext = { options: extend( { @@ -100,10 +94,7 @@ function createCodegenContext(ir: RootIRNode, options: CodegenOptions) { ), source: ir.source, - code: '', - column: 1, - line: 1, - offset: 0, + code, indentLevel: 0, helpers, @@ -117,95 +108,35 @@ function createCodegenContext(ir: RootIRNode, options: CodegenOptions) { return `_${name}` }, - push(code, newlineIndex = NewlineType.None, loc, name) { - context.code += code - if (!__BROWSER__ && context.map) { - 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) - } - } + push, + newline() { + return [`\n${` `.repeat(context.indentLevel)}`, NewlineType.Start] }, - newline(code, newlineIndex, node) { - context.push(`\n${` `.repeat(context.indentLevel)}`, NewlineType.Start) - code && context.push(code, newlineIndex, node) - }, - pushMulti([left, right, seg], ...fns) { + multi([left, right, seg], ...fns) { + const frag: CodeFragment[] = [] fns = fns.filter(Boolean) - context.push(left) + frag.push(left) for (const [i, fn] of fns.entries()) { - if (isString(fn)) context.push(fn) - else (fn as () => void)() - if (seg && i < fns.length - 1) context.push(seg) + if (fn) { + frag.push(...fn) + if (i < fns.length - 1) frag.push(seg) + } } - context.push(right) + frag.push(right) + return frag }, - pushCall(name, ...args) { - context.push(name) - context.pushMulti(['(', ')', ', '], ...args) + call(name, ...args) { + return [name, ...context.multi(['(', ')', ', '], ...args)] }, withIndent(fn) { ++context.indentLevel - fn() + const ret = fn() --context.indentLevel + return ret }, } 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) { // lazy require source-map implementation, only in non-browser builds context.map = new SourceMapGenerator() @@ -228,37 +159,27 @@ export function generate( options: CodegenOptions = {}, ): VaporCodegenResult { const ctx = createCodegenContext(ir, options) - const { push, withIndent, newline, helpers, vaporHelper, vaporHelpers } = ctx + const { push, withIndent, newline, helpers, vaporHelpers } = ctx const functionName = 'render' const isSetupInlined = !!options.inline if (isSetupInlined) { push(`(() => {`) } else { - // placeholder for preamble - newline() - newline(`export function ${functionName}(_ctx) {`) + push( + // placeholder for preamble + newline(), + newline(), + `export function ${functionName}(_ctx) {`, + ) } withIndent(() => { - ir.template.forEach((template, i) => { - if (template.type === IRNodeTypes.TEMPLATE_FACTORY) { - // TODO source map? - newline( - `const t${i} = ${vaporHelper('template')}(${JSON.stringify( - template.template, - )})`, - ) - } else { - // fragment - newline(`const t${i} = ${vaporHelper('fragment')}()`) - } - }) - - genBlockFunctionContent(ir, ctx) + ir.template.forEach((template, i) => push(...genTemplate(template, i, ctx))) + push(...genBlockFunctionContent(ir, ctx)) }) - newline() + push(newline()) if (isSetupInlined) { push('})()') } else { @@ -276,12 +197,13 @@ export function generate( .map(h => `${h} as _${h}`) .join(', ')} } from 'vue';` + let codegen = genCodeFragment(ctx) if (!isSetupInlined) { - ctx.code = preamble + ctx.code + codegen = preamble + codegen } return { - code: ctx.code, + code: codegen, ast: ir, preamble, 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[]) { let code = '' let offset = 0 @@ -320,7 +318,10 @@ function genChildren(children: IRDynamicInfo[]) { return `{${code}}` } -function genOperation(oper: OperationNode, context: CodegenContext) { +function genOperation( + oper: OperationNode, + context: CodegenContext, +): CodeFragment[] { // TODO: cache old value switch (oper.type) { case IRNodeTypes.SET_PROP: @@ -347,22 +348,35 @@ function genOperation(oper: OperationNode, context: CodegenContext) { return genIf(oper, context) case IRNodeTypes.WITH_DIRECTIVE: // generated, skip - return + break default: return checkNever(oper) } + + return [] +} + +export function buildCodeFragment() { + const frag: CodeFragment[] = [] + const push = frag.push.bind(frag) + return [frag, push] as const } export function genBlockFunctionContent( ir: BlockFunctionIRNode | RootIRNode, ctx: CodegenContext, -) { +): CodeFragment[] { 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) 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( @@ -370,24 +384,24 @@ export function genBlockFunctionContent( oper.type === IRNodeTypes.WITH_DIRECTIVE, ) for (const directives of groupDirective(directiveOps)) { - genWithDirective(directives, ctx) + push(...genWithDirective(directives, ctx)) } for (const operation of ir.operation) { - genOperation(operation, ctx) + push(...genOperation(operation, ctx)) } for (const { operations } of ir.effect) { - newline(`${vaporHelper('renderEffect')}(() => {`) + push(newline(), `${vaporHelper('renderEffect')}(() => {`) withIndent(() => { - for (const operation of operations) { - genOperation(operation, ctx) - } + operations.forEach(op => push(...genOperation(op, ctx))) }) - newline('})') + push(newline(), '})') } - newline(`return n${ir.dynamic.id}`) + push(newline(), `return n${ir.dynamic.id}`) + + return frag } function groupDirective(ops: WithDirectiveIRNode[]): WithDirectiveIRNode[][] { diff --git a/packages/compiler-vapor/src/generators/directive.ts b/packages/compiler-vapor/src/generators/directive.ts index 2c9d9143c7..2367bb8d9e 100644 --- a/packages/compiler-vapor/src/generators/directive.ts +++ b/packages/compiler-vapor/src/generators/directive.ts @@ -1,86 +1,73 @@ import { createSimpleExpression, isSimpleIdentifier } from '@vue/compiler-dom' import { camelize } from '@vue/shared' import { genExpression } from './expression' -import type { CodegenContext } from '../generate' +import type { CodeFragment, CodegenContext } from '../generate' import type { WithDirectiveIRNode } from '../ir' export function genWithDirective( opers: WithDirectiveIRNode[], context: CodegenContext, ) { - const { - push, - newline, - pushCall, - pushMulti, - vaporHelper, - options: { bindingMetadata }, - } = context + const { newline, call, multi, vaporHelper } = context - newline() - pushCall( - vaporHelper('withDirectives'), - // 1st arg: node - `n${opers[0].element}`, - // 2nd arg: directives - () => { - // directive - pushMulti( - ['[', ']', ', '], - ...opers.map(oper => () => { - push('[') + const element = `n${opers[0].element}` + const directiveItems = opers.map(genDirective) + const directives = multi(['[', ']', ', '], ...directiveItems) - const { dir, builtin } = oper - if (dir.name === 'show') { - push(vaporHelper('vShow')) - } else if (builtin) { - push(vaporHelper(builtin)) - } else { - const directiveReference = camelize(`v-${dir.name}`) - // TODO resolve directive - if (bindingMetadata[directiveReference]) { - const directiveExpression = - createSimpleExpression(directiveReference) - directiveExpression.ast = null - genExpression(directiveExpression, context) - } else { - push(vaporHelper(`resolveDirective("${directiveReference}")`)) - } - } + return [ + newline(), + ...call(vaporHelper('withDirectives'), [element], directives), + ] - if (dir.exp) { - push(', () => ') - genExpression(dir.exp, context) - } else if (dir.arg || dir.modifiers.length) { - push(', void 0') - } + function genDirective({ dir, builtin }: WithDirectiveIRNode): CodeFragment[] { + const NULL = ['void 0'] - if (dir.arg) { - push(', ') - genExpression(dir.arg, context) - } else if (dir.modifiers.length) { - push(', 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 - if (dir.modifiers.length) { - push(', ') - push('{ ') - push(genDirectiveModifiers(dir.modifiers)) - push(' }') - } + return multi(['[', ']', ', '], directive, value, argument, modifiers) - push(']') - }), - ) - }, - ) -} + function genDirective(): CodeFragment[] { + const { + vaporHelper, + options: { bindingMetadata }, + } = context + if (dir.name === 'show') { + return [vaporHelper('vShow')] + } else if (builtin) { + return [vaporHelper(builtin)] + } else { + const directiveReference = camelize(`v-${dir.name}`) + // TODO resolve directive + if (bindingMetadata[directiveReference]) { + const directiveExpression = createSimpleExpression(directiveReference) + directiveExpression.ast = null + return genExpression(directiveExpression, context) + } else { + return [vaporHelper(`resolveDirective("${directiveReference}")`)] + } + } + } -function genDirectiveModifiers(modifiers: string[]) { - return modifiers - .map( - value => - `${isSimpleIdentifier(value) ? value : JSON.stringify(value)}: true`, - ) - .join(', ') + function genDirectiveModifiers() { + return dir.modifiers + .map( + value => + `${isSimpleIdentifier(value) ? value : JSON.stringify(value)}: true`, + ) + .join(', ') + } + } } diff --git a/packages/compiler-vapor/src/generators/dom.ts b/packages/compiler-vapor/src/generators/dom.ts index 17a3e27cc7..f30b8c6d8f 100644 --- a/packages/compiler-vapor/src/generators/dom.ts +++ b/packages/compiler-vapor/src/generators/dom.ts @@ -1,4 +1,4 @@ -import type { CodegenContext } from '../generate' +import type { CodeFragment, CodegenContext } from '../generate' import type { AppendNodeIRNode, InsertNodeIRNode, @@ -7,35 +7,47 @@ import type { export function genInsertNode( oper: InsertNodeIRNode, - { newline, pushCall, vaporHelper }: CodegenContext, -) { + { newline, call, vaporHelper }: CodegenContext, +): CodeFragment[] { const elements = ([] as number[]).concat(oper.element) let element = elements.map(el => `n${el}`).join(', ') if (elements.length > 1) element = `[${element}]` - newline() - pushCall(vaporHelper('insert'), element, `n${oper.parent}`, `n${oper.anchor}`) + return [ + newline(), + ...call( + vaporHelper('insert'), + [element], + [`n${oper.parent}`], + [`n${oper.anchor}`], + ), + ] } export function genPrependNode( oper: PrependNodeIRNode, - { newline, pushCall, vaporHelper }: CodegenContext, -) { - newline() - pushCall( - vaporHelper('prepend'), - `n${oper.parent}`, - oper.elements.map(el => `n${el}`).join(', '), - ) + { newline, call, vaporHelper }: CodegenContext, +): CodeFragment[] { + return [ + newline(), + ...call( + vaporHelper('prepend'), + [`n${oper.parent}`], + ...oper.elements.map(el => [`n${el}`]), + ), + ] } export function genAppendNode( oper: AppendNodeIRNode, - { newline, pushCall, vaporHelper }: CodegenContext, -) { + { newline, call, vaporHelper }: CodegenContext, +): CodeFragment[] { newline() - pushCall( - vaporHelper('append'), - `n${oper.parent}`, - oper.elements.map(el => `n${el}`).join(', '), - ) + return [ + newline(), + ...call( + vaporHelper('append'), + [`n${oper.parent}`], + ...oper.elements.map(el => [`n${el}`]), + ), + ] } diff --git a/packages/compiler-vapor/src/generators/event.ts b/packages/compiler-vapor/src/generators/event.ts index f369315d99..7d79b579ec 100644 --- a/packages/compiler-vapor/src/generators/event.ts +++ b/packages/compiler-vapor/src/generators/event.ts @@ -1,5 +1,5 @@ import { isMemberExpression } from '@vue/compiler-dom' -import type { CodegenContext } from '../generate' +import type { CodeFragment, CodegenContext } from '../generate' import type { SetEventIRNode } from '../ir' import { genExpression } from './expression' @@ -7,60 +7,38 @@ import { genExpression } from './expression' const fnExpRE = /^\s*([\w$_]+|(async\s*)?\([^)]*?\))\s*(:[^=]+)?=>|^\s*(async\s+)?function(?:\s+[\w$]+)?\s*\(/ -export function genSetEvent(oper: SetEventIRNode, context: CodegenContext) { - const { - vaporHelper, - push, - newline, - pushMulti, - pushCall, - options: ctxOptions, - } = context +export function genSetEvent( + oper: SetEventIRNode, + context: CodegenContext, +): CodeFragment[] { + const { vaporHelper, newline, call, options: ctxOptions } = context const { keys, nonKeys, options } = oper.modifiers - newline() - pushCall( - vaporHelper('on'), - // 1st arg: event name - () => push(`n${oper.element}`), - // 2nd arg: event name - () => { - if (oper.keyOverride) { - const find = JSON.stringify(oper.keyOverride[0]) - const replacement = JSON.stringify(oper.keyOverride[1]) - pushMulti(['(', ')'], () => genExpression(oper.key, context)) - push(` === ${find} ? ${replacement} : `) - pushMulti(['(', ')'], () => genExpression(oper.key, context)) - } else { - 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() + const name = genName() + const handler = genFinalizedHandler() + const opt = !!options.length && [ + `{ ${options.map(v => `${v}: true`).join(', ')} }`, + ] - ;(keys.length ? pushWithKeys : pushNoop)(() => - (nonKeys.length ? pushWithModifiers : pushNoop)(() => { - genEventHandler() - }), - ) - }, - // 4th arg, gen options - !!options.length && - (() => push(`{ ${options.map(v => `${v}: true`).join(', ')} }`)), - ) + 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) { + const find = JSON.stringify(oper.keyOverride[0]) + const replacement = JSON.stringify(oper.keyOverride[1]) + const wrapped: CodeFragment[] = ['(', ...expr, ')'] + return [...wrapped, ` === ${find} ? ${replacement} : `, ...wrapped] + } else { + return genExpression(oper.key, context) + } + } - function genEventHandler() { + function genEventHandler(): CodeFragment[] { const exp = oper.value if (exp && exp.content.trim()) { const isMemberExp = isMemberExpression(exp.content, ctxOptions) @@ -68,24 +46,52 @@ export function genSetEvent(oper: SetEventIRNode, context: CodegenContext) { const hasMultipleStatements = exp.content.includes(`;`) if (isInlineStatement) { - push('$event => ') - push(hasMultipleStatements ? '{' : '(') const knownIds = Object.create(null) knownIds['$event'] = 1 - genExpression(exp, context, knownIds) - push(hasMultipleStatements ? '}' : ')') - } else if (isMemberExp) { - push('(...args) => (') - genExpression(exp, context) - push(' && ') - genExpression(exp, context) - push('(...args))') + + return [ + '$event => ', + hasMultipleStatements ? '{' : '(', + ...genExpression(exp, context, knownIds), + hasMultipleStatements ? '}' : ')', + ] } else { - genExpression(exp, context) + const expr = genExpression(exp, context) + if (isMemberExp) { + return ['(...args) => (', ...expr, ' && ', ...expr, '(...args))'] + } else { + return expr + } } - } else { - push('() => {}') } + 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 } } diff --git a/packages/compiler-vapor/src/generators/expression.ts b/packages/compiler-vapor/src/generators/expression.ts index 90019290ec..ba8c1d71f3 100644 --- a/packages/compiler-vapor/src/generators/expression.ts +++ b/packages/compiler-vapor/src/generators/expression.ts @@ -8,23 +8,27 @@ import { import { isGloballyAllowed, isString, makeMap } from '@vue/shared' import type { Identifier } from '@babel/types' import type { IRExpression } from '../ir' -import type { CodegenContext } from '../generate' +import { + type CodeFragment, + type CodegenContext, + buildCodeFragment, +} from '../generate' export function genExpression( node: IRExpression, context: CodegenContext, knownIds: Record = Object.create(null), -): void { +): CodeFragment[] { const { - push, options: { prefixIdentifiers }, } = context - if (isString(node)) return push(node) + if (isString(node)) return [node] const { content: rawExpr, ast, isStatic, loc } = node if (isStatic) { - return push(JSON.stringify(rawExpr), NewlineType.None, loc) + return [[JSON.stringify(rawExpr), NewlineType.None, loc]] } + if ( __BROWSER__ || !prefixIdentifiers || @@ -34,12 +38,12 @@ export function genExpression( isGloballyAllowed(rawExpr) || isLiteralWhitelisted(rawExpr) ) { - return push(rawExpr, NewlineType.None, loc) + return [[rawExpr, NewlineType.None, loc]] } if (ast === null) { // the expression is a simple identifier - return genIdentifier(rawExpr, context, loc) + return [genIdentifier(rawExpr, context, loc)] } const ids: Identifier[] = [] @@ -55,6 +59,7 @@ export function genExpression( ) if (ids.length) { ids.sort((a, b) => a.start! - b.start!) + const [frag, push] = buildCodeFragment() ids.forEach((id, i) => { // range is offset by -1 due to the wrapping parens when parsed const start = id.start! - 1 @@ -62,21 +67,24 @@ export function genExpression( const last = ids[i - 1] 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) - genIdentifier(source, context, { - start: advancePositionWithClone(node.loc.start, source, start), - end: advancePositionWithClone(node.loc.start, source, end), - source, - }) + push( + genIdentifier(source, context, { + start: advancePositionWithClone(node.loc.start, source, start), + end: advancePositionWithClone(node.loc.start, source, end), + source, + }), + ) if (i === ids.length - 1 && end < rawExpr.length) { - push(rawExpr.slice(end), NewlineType.Unknown) + push([rawExpr.slice(end), NewlineType.Unknown]) } }) + return frag } else { - push(rawExpr, NewlineType.Unknown) + return [[rawExpr, NewlineType.Unknown]] } } @@ -84,9 +92,9 @@ const isLiteralWhitelisted = /*#__PURE__*/ makeMap('true,false,null,this') function genIdentifier( id: string, - { options, vaporHelper, push }: CodegenContext, + { options, vaporHelper }: CodegenContext, loc?: SourceLocation, -): void { +): CodeFragment { const { inline, bindingMetadata } = options let name: string | undefined = id if (inline) { @@ -102,5 +110,5 @@ function genIdentifier( } else { id = `_ctx.${id}` } - push(id, NewlineType.None, loc, name) + return [id, NewlineType.None, loc, name] } diff --git a/packages/compiler-vapor/src/generators/html.ts b/packages/compiler-vapor/src/generators/html.ts index 3ca7c56956..f6188fd72c 100644 --- a/packages/compiler-vapor/src/generators/html.ts +++ b/packages/compiler-vapor/src/generators/html.ts @@ -1,11 +1,18 @@ -import type { CodegenContext } from '../generate' +import type { CodeFragment, CodegenContext } from '../generate' import type { SetHtmlIRNode } from '../ir' import { genExpression } from './expression' -export function genSetHtml(oper: SetHtmlIRNode, context: CodegenContext) { - const { newline, pushCall, vaporHelper } = context - newline() - pushCall(vaporHelper('setHtml'), `n${oper.element}`, () => - genExpression(oper.value, context), - ) +export function genSetHtml( + oper: SetHtmlIRNode, + context: CodegenContext, +): CodeFragment[] { + const { newline, call, vaporHelper } = context + return [ + newline(), + ...call( + vaporHelper('setHtml'), + [`n${oper.element}`], + genExpression(oper.value, context), + ), + ] } diff --git a/packages/compiler-vapor/src/generators/if.ts b/packages/compiler-vapor/src/generators/if.ts index 72b2b09919..69848b424f 100644 --- a/packages/compiler-vapor/src/generators/if.ts +++ b/packages/compiler-vapor/src/generators/if.ts @@ -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 { genExpression } from './expression' @@ -6,43 +11,45 @@ export function genIf( oper: IfIRNode, context: CodegenContext, isNested = false, -) { - const { pushCall, vaporHelper, newline, push } = context +): CodeFragment[] { + const { call, vaporHelper, newline } = context const { condition, positive, negative } = oper + const [frag, push] = buildCodeFragment() - let positiveArg = () => genBlockFunction(positive, context) - let negativeArg: false | (() => void) = false + const conditionExpr: CodeFragment[] = [ + '() => (', + ...genExpression(condition, context), + ')', + ] + + let positiveArg = genBlockFunction(positive, context) + let negativeArg: false | CodeFragment[] = false if (negative) { if (negative.type === IRNodeTypes.BLOCK_FUNCTION) { - negativeArg = () => genBlockFunction(negative, context) + negativeArg = genBlockFunction(negative, context) } else { - negativeArg = () => { - push('() => ') - genIf(negative!, context, true) - } + negativeArg = ['() => ', ...genIf(negative!, context, true)] } } - if (!isNested) newline(`const n${oper.id} = `) - pushCall( - vaporHelper('createIf'), - () => { - push('() => (') - genExpression(condition, context) - push(')') - }, - positiveArg, - negativeArg, + if (!isNested) push(newline(), `const n${oper.id} = `) + push( + ...call(vaporHelper('createIf'), conditionExpr, positiveArg, negativeArg), ) -} -function genBlockFunction(oper: BlockFunctionIRNode, context: CodegenContext) { - const { newline, push, withIndent } = context + return frag +} - push('() => {') - withIndent(() => { - genBlockFunctionContent(oper, context) - }) - newline('}') +function genBlockFunction( + oper: BlockFunctionIRNode, + context: CodegenContext, +): CodeFragment[] { + const { newline, withIndent } = context + return [ + '() => {', + ...withIndent(() => genBlockFunctionContent(oper, context)), + newline(), + '}', + ] } diff --git a/packages/compiler-vapor/src/generators/modelValue.ts b/packages/compiler-vapor/src/generators/modelValue.ts index f596cb2c9a..b50d223348 100644 --- a/packages/compiler-vapor/src/generators/modelValue.ts +++ b/packages/compiler-vapor/src/generators/modelValue.ts @@ -1,41 +1,41 @@ import { camelize, isString } from '@vue/shared' import { genExpression } from './expression' import type { SetModelValueIRNode } from '../ir' -import type { CodegenContext } from '../generate' +import type { CodeFragment, CodegenContext } from '../generate' export function genSetModelValue( oper: SetModelValueIRNode, context: CodegenContext, -) { +): CodeFragment[] { const { vaporHelper, - push, newline, - pushCall, + call, options: { isTS }, } = context - newline() - pushCall( - vaporHelper('on'), - // 1st arg: event name - () => push(`n${oper.element}`), - // 2nd arg: event name - () => { - if (isString(oper.key)) { - push(JSON.stringify(`update:${camelize(oper.key)}`)) - } else { - push('`update:${') - genExpression(oper.key, context) - push('}`') - } - }, - // 3rd arg: event handler - () => { - push((isTS ? `($event: any)` : `$event`) + ' => ((') + const name = genName() + const handler = genHandler() + + return [ + newline(), + ...call(vaporHelper('on'), [`n${oper.element}`], name, handler), + ] + + function genName(): CodeFragment[] { + if (isString(oper.key)) { + return [JSON.stringify(`update:${camelize(oper.key)}`)] + } else { + return ['`update:${', ...genExpression(oper.key, context), '}`'] + } + } + + function genHandler(): CodeFragment[] { + return [ + (isTS ? `($event: any)` : `$event`) + ' => ((', // TODO handle not a ref - genExpression(oper.value, context) - push(') = $event)') - }, - ) + ...genExpression(oper.value, context), + ') = $event)', + ] + } } diff --git a/packages/compiler-vapor/src/generators/prop.ts b/packages/compiler-vapor/src/generators/prop.ts index 204c2f2c5b..e8f17e9345 100644 --- a/packages/compiler-vapor/src/generators/prop.ts +++ b/packages/compiler-vapor/src/generators/prop.ts @@ -1,14 +1,16 @@ -import type { CodegenContext } from '../generate' +import type { CodeFragment, CodegenContext } from '../generate' import type { SetPropIRNode } from '../ir' import { genExpression } from './expression' import { isString } from '@vue/shared' -export function genSetProp(oper: SetPropIRNode, context: CodegenContext) { - const { pushCall, pushMulti, newline, vaporHelper, helper } = context +export function genSetProp( + oper: SetPropIRNode, + context: CodegenContext, +): CodeFragment[] { + const { call, newline, vaporHelper, helper } = context - newline() - - const element = `n${oper.element}` + const element = [`n${oper.element}`] + const expr = genExpression(oper.key, context) // fast path for static props if (isString(oper.key) || oper.key.isStatic) { @@ -27,40 +29,35 @@ export function genSetProp(oper: SetPropIRNode, context: CodegenContext) { } if (helperName) { - pushCall( - vaporHelper(helperName), - element, - omitKey - ? false - : () => { - const expr = () => genExpression(oper.key, context) - if (oper.runtimeCamelize) { - pushCall(helper('camelize'), expr) - } else { - expr() - } - }, - () => genExpression(oper.value, context), - ) - return + return [ + newline(), + ...call( + vaporHelper(helperName), + element, + omitKey ? false : expr, + genExpression(oper.value, context), + ), + ] } } - pushCall( - vaporHelper('setDynamicProp'), - element, - // 2. key name - () => { - if (oper.runtimeCamelize) { - pushCall(helper('camelize'), () => genExpression(oper.key, context)) - } else if (oper.modifier) { - pushMulti([`\`${oper.modifier}\${`, `}\``], () => - genExpression(oper.key, context), - ) - } else { - genExpression(oper.key, context) - } - }, - () => genExpression(oper.value, context), - ) + return [ + newline(), + ...call( + vaporHelper('setDynamicProp'), + element, + genDynamicKey(), + genExpression(oper.value, context), + ), + ] + + function genDynamicKey(): CodeFragment[] { + if (oper.runtimeCamelize) { + return call(helper('camelize'), expr) + } else if (oper.modifier) { + return [`\`${oper.modifier}\${`, ...expr, `}\``] + } else { + return expr + } + } } diff --git a/packages/compiler-vapor/src/generators/ref.ts b/packages/compiler-vapor/src/generators/ref.ts index bcd0c0880d..896443037f 100644 --- a/packages/compiler-vapor/src/generators/ref.ts +++ b/packages/compiler-vapor/src/generators/ref.ts @@ -1,11 +1,18 @@ -import type { CodegenContext } from '../generate' -import type { SetRefIRNode } from '../ir' import { genExpression } from './expression' +import type { CodeFragment, CodegenContext } from '../generate' +import type { SetRefIRNode } from '../ir' -export function genSetRef(oper: SetRefIRNode, context: CodegenContext) { - const { newline, pushCall, vaporHelper } = context - newline() - pushCall(vaporHelper('setRef'), `n${oper.element}`, () => - genExpression(oper.value, context), - ) +export function genSetRef( + oper: SetRefIRNode, + context: CodegenContext, +): CodeFragment[] { + const { newline, call, vaporHelper } = context + return [ + newline(), + ...call( + vaporHelper('setRef'), + [`n${oper.element}`], + genExpression(oper.value, context), + ), + ] } diff --git a/packages/compiler-vapor/src/generators/template.ts b/packages/compiler-vapor/src/generators/template.ts new file mode 100644 index 0000000000..bbf79c061c --- /dev/null +++ b/packages/compiler-vapor/src/generators/template.ts @@ -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')}()`] + } +} diff --git a/packages/compiler-vapor/src/generators/text.ts b/packages/compiler-vapor/src/generators/text.ts index 7752acd971..4ece211195 100644 --- a/packages/compiler-vapor/src/generators/text.ts +++ b/packages/compiler-vapor/src/generators/text.ts @@ -1,22 +1,30 @@ -import type { CodegenContext } from '../generate' +import type { CodeFragment, CodegenContext } from '../generate' import type { CreateTextNodeIRNode, SetTextIRNode } from '../ir' import { genExpression } from './expression' -export function genSetText(oper: SetTextIRNode, context: CodegenContext) { - const { pushCall, newline, vaporHelper } = context - newline() - pushCall(vaporHelper('setText'), `n${oper.element}`, () => - genExpression(oper.value, context), - ) +export function genSetText( + oper: SetTextIRNode, + context: CodegenContext, +): CodeFragment[] { + const { call, newline, vaporHelper } = context + return [ + newline(), + ...call( + vaporHelper('setText'), + [`n${oper.element}`], + genExpression(oper.value, context), + ), + ] } export function genCreateTextNode( oper: CreateTextNodeIRNode, context: CodegenContext, -) { - const { newline, pushCall, vaporHelper } = context - newline(`const n${oper.id} = `) - pushCall(vaporHelper('createTextNode'), () => - genExpression(oper.value, context), - ) +): CodeFragment[] { + const { newline, call, vaporHelper } = context + return [ + newline(), + `const n${oper.id} = `, + ...call(vaporHelper('createTextNode'), genExpression(oper.value, context)), + ] }