diff --git a/packages/compiler-vapor/__tests__/transforms/__snapshots__/transformSlotOutlet.spec.ts.snap b/packages/compiler-vapor/__tests__/transforms/__snapshots__/transformSlotOutlet.spec.ts.snap new file mode 100644 index 000000000..72b9b95f1 --- /dev/null +++ b/packages/compiler-vapor/__tests__/transforms/__snapshots__/transformSlotOutlet.spec.ts.snap @@ -0,0 +1,136 @@ +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html + +exports[`compiler: transform outlets > default slot outlet 1`] = ` +"import { createSlot as _createSlot } from 'vue/vapor'; + +export function render(_ctx) { + const n0 = _createSlot("default", null) + return n0 +}" +`; + +exports[`compiler: transform outlets > default slot outlet with fallback 1`] = ` +"import { createSlot as _createSlot, template as _template } from 'vue/vapor'; +const t0 = _template("
") + +export function render(_ctx) { + const n0 = _createSlot("default", null, () => { + const n2 = t0() + return n2 + }) + return n0 +}" +`; + +exports[`compiler: transform outlets > default slot outlet with props 1`] = ` +"import { createSlot as _createSlot } from 'vue/vapor'; + +export function render(_ctx) { + const n0 = _createSlot("default", [ + { + foo: () => ("bar"), + baz: () => (_ctx.qux), + fooBar: () => (_ctx.foo-_ctx.bar) + } + ]) + return n0 +}" +`; + +exports[`compiler: transform outlets > dynamically named slot outlet 1`] = ` +"import { createSlot as _createSlot } from 'vue/vapor'; + +export function render(_ctx) { + const n0 = _createSlot(() => (_ctx.foo + _ctx.bar), null) + return n0 +}" +`; + +exports[`compiler: transform outlets > dynamically named slot outlet with v-bind shorthand 1`] = ` +"import { createSlot as _createSlot } from 'vue/vapor'; + +export function render(_ctx) { + const n0 = _createSlot(() => (_ctx.name), null) + return n0 +}" +`; + +exports[`compiler: transform outlets > error on unexpected custom directive on 1`] = ` +"import { createSlot as _createSlot } from 'vue/vapor'; + +export function render(_ctx) { + const n0 = _createSlot("default", null) + return n0 +}" +`; + +exports[`compiler: transform outlets > error on unexpected custom directive with v-show on 1`] = ` +"import { createSlot as _createSlot } from 'vue/vapor'; + +export function render(_ctx) { + const n0 = _createSlot("default", null) + return n0 +}" +`; + +exports[`compiler: transform outlets > named slot outlet with fallback 1`] = ` +"import { createSlot as _createSlot, template as _template } from 'vue/vapor'; +const t0 = _template("
") + +export function render(_ctx) { + const n0 = _createSlot("foo", null, () => { + const n2 = t0() + return n2 + }) + return n0 +}" +`; + +exports[`compiler: transform outlets > statically named slot outlet 1`] = ` +"import { createSlot as _createSlot } from 'vue/vapor'; + +export function render(_ctx) { + const n0 = _createSlot("foo", null) + return n0 +}" +`; + +exports[`compiler: transform outlets > statically named slot outlet with props 1`] = ` +"import { createSlot as _createSlot } from 'vue/vapor'; + +export function render(_ctx) { + const n0 = _createSlot("foo", [ + { + foo: () => ("bar"), + baz: () => (_ctx.qux) + } + ]) + return n0 +}" +`; + +exports[`compiler: transform outlets > statically named slot outlet with v-bind="obj" 1`] = ` +"import { createSlot as _createSlot } from 'vue/vapor'; + +export function render(_ctx) { + const n0 = _createSlot("foo", [ + { foo: () => ("bar") }, + () => (_ctx.obj), + { baz: () => (_ctx.qux) } + ]) + return n0 +}" +`; + +exports[`compiler: transform outlets > statically named slot outlet with v-on 1`] = ` +"import { createSlot as _createSlot, toHandlers as _toHandlers } from 'vue/vapor'; + +export function render(_ctx) { + const n0 = _createSlot("default", [ + { onClick: () => _ctx.foo }, + () => (_toHandlers(_ctx.bar)), + { baz: () => (_ctx.qux) } + ]) + return n0 +}" +`; diff --git a/packages/compiler-vapor/__tests__/transforms/transformSlotOutlet.spec.ts b/packages/compiler-vapor/__tests__/transforms/transformSlotOutlet.spec.ts new file mode 100644 index 000000000..aee60e5b3 --- /dev/null +++ b/packages/compiler-vapor/__tests__/transforms/transformSlotOutlet.spec.ts @@ -0,0 +1,258 @@ +import { ErrorCodes, NodeTypes } from '@vue/compiler-core' +import { + IRNodeTypes, + transformChildren, + transformElement, + transformSlotOutlet, + transformText, + transformVBind, + transformVOn, + transformVShow, +} from '../../src' +import { makeCompile } from './_utils' + +const compileWithSlotsOutlet = makeCompile({ + nodeTransforms: [ + transformText, + transformSlotOutlet, + transformElement, + transformChildren, + ], + directiveTransforms: { + bind: transformVBind, + on: transformVOn, + show: transformVShow, + }, +}) + +describe('compiler: transform outlets', () => { + test('default slot outlet', () => { + const { ir, code, vaporHelpers } = compileWithSlotsOutlet(``) + expect(code).toMatchSnapshot() + expect(vaporHelpers).toContain('createSlot') + expect(ir.block.effect).toEqual([]) + expect(ir.block.operation).toMatchObject([ + { + type: IRNodeTypes.SLOT_OUTLET_NODE, + id: 0, + name: { + type: NodeTypes.SIMPLE_EXPRESSION, + content: 'default', + isStatic: true, + }, + props: [], + fallback: undefined, + }, + ]) + }) + + test('statically named slot outlet', () => { + const { ir, code } = compileWithSlotsOutlet(``) + expect(code).toMatchSnapshot() + expect(ir.block.operation).toMatchObject([ + { + type: IRNodeTypes.SLOT_OUTLET_NODE, + id: 0, + name: { + type: NodeTypes.SIMPLE_EXPRESSION, + content: 'foo', + isStatic: true, + }, + }, + ]) + }) + + test('dynamically named slot outlet', () => { + const { ir, code } = compileWithSlotsOutlet(``) + expect(code).toMatchSnapshot() + expect(ir.block.operation).toMatchObject([ + { + type: IRNodeTypes.SLOT_OUTLET_NODE, + id: 0, + name: { + type: NodeTypes.SIMPLE_EXPRESSION, + content: 'foo + bar', + isStatic: false, + }, + }, + ]) + }) + + test('dynamically named slot outlet with v-bind shorthand', () => { + const { ir, code } = compileWithSlotsOutlet(``) + expect(code).toMatchSnapshot() + expect(ir.block.operation).toMatchObject([ + { + type: IRNodeTypes.SLOT_OUTLET_NODE, + id: 0, + name: { + type: NodeTypes.SIMPLE_EXPRESSION, + content: 'name', + isStatic: false, + }, + }, + ]) + }) + + test('default slot outlet with props', () => { + const { ir, code } = compileWithSlotsOutlet( + ``, + ) + expect(code).toMatchSnapshot() + expect(ir.block.operation).toMatchObject([ + { + type: IRNodeTypes.SLOT_OUTLET_NODE, + name: { content: 'default' }, + props: [ + [ + { key: { content: 'foo' }, values: [{ content: 'bar' }] }, + { key: { content: 'baz' }, values: [{ content: 'qux' }] }, + { key: { content: 'fooBar' }, values: [{ content: 'foo-bar' }] }, + ], + ], + }, + ]) + }) + + test('statically named slot outlet with props', () => { + const { ir, code } = compileWithSlotsOutlet( + ``, + ) + expect(code).toMatchSnapshot() + expect(ir.block.operation).toMatchObject([ + { + type: IRNodeTypes.SLOT_OUTLET_NODE, + name: { content: 'foo' }, + props: [ + [ + { key: { content: 'foo' }, values: [{ content: 'bar' }] }, + { key: { content: 'baz' }, values: [{ content: 'qux' }] }, + ], + ], + }, + ]) + }) + + test('statically named slot outlet with v-bind="obj"', () => { + const { ir, code } = compileWithSlotsOutlet( + ``, + ) + expect(code).toMatchSnapshot() + expect(ir.block.operation).toMatchObject([ + { + type: IRNodeTypes.SLOT_OUTLET_NODE, + name: { content: 'foo' }, + props: [ + [{ key: { content: 'foo' }, values: [{ content: 'bar' }] }], + { value: { content: 'obj', isStatic: false } }, + [{ key: { content: 'baz' }, values: [{ content: 'qux' }] }], + ], + }, + ]) + }) + + test('statically named slot outlet with v-on', () => { + const { ir, code } = compileWithSlotsOutlet( + ``, + ) + expect(code).toMatchSnapshot() + expect(ir.block.operation).toMatchObject([ + { + type: IRNodeTypes.SLOT_OUTLET_NODE, + props: [ + [{ key: { content: 'click' }, values: [{ content: 'foo' }] }], + { value: { content: 'bar' }, handler: true }, + [{ key: { content: 'baz' }, values: [{ content: 'qux' }] }], + ], + }, + ]) + }) + + test('default slot outlet with fallback', () => { + const { ir, code } = compileWithSlotsOutlet(`
`) + expect(code).toMatchSnapshot() + expect(ir.template[0]).toMatchObject('
') + expect(ir.block.operation).toMatchObject([ + { + type: IRNodeTypes.SLOT_OUTLET_NODE, + id: 0, + name: { content: 'default' }, + fallback: { + type: IRNodeTypes.BLOCK, + dynamic: { + children: [{ template: 0, id: 2 }], + }, + returns: [2], + }, + }, + ]) + }) + + test('named slot outlet with fallback', () => { + const { ir, code } = compileWithSlotsOutlet( + `
`, + ) + expect(code).toMatchSnapshot() + expect(ir.template[0]).toMatchObject('
') + expect(ir.block.operation).toMatchObject([ + { + type: IRNodeTypes.SLOT_OUTLET_NODE, + id: 0, + name: { content: 'foo' }, + fallback: { + type: IRNodeTypes.BLOCK, + dynamic: { + children: [{ template: 0, id: 2 }], + }, + returns: [2], + }, + }, + ]) + }) + + test('error on unexpected custom directive on ', () => { + const onError = vi.fn() + const source = `` + const index = source.indexOf('v-foo') + const { code } = compileWithSlotsOutlet(source, { onError }) + expect(code).toMatchSnapshot() + expect(onError.mock.calls[0][0]).toMatchObject({ + code: ErrorCodes.X_V_SLOT_UNEXPECTED_DIRECTIVE_ON_SLOT_OUTLET, + loc: { + start: { + offset: index, + line: 1, + column: index + 1, + }, + end: { + offset: index + 5, + line: 1, + column: index + 6, + }, + }, + }) + }) + + test('error on unexpected custom directive with v-show on ', () => { + const onError = vi.fn() + const source = `` + const index = source.indexOf('v-show="ok"') + const { code } = compileWithSlotsOutlet(source, { onError }) + expect(code).toMatchSnapshot() + expect(onError.mock.calls[0][0]).toMatchObject({ + code: ErrorCodes.X_V_SLOT_UNEXPECTED_DIRECTIVE_ON_SLOT_OUTLET, + loc: { + start: { + offset: index, + line: 1, + column: index + 1, + }, + end: { + offset: index + 11, + line: 1, + column: index + 12, + }, + }, + }) + }) +}) diff --git a/packages/compiler-vapor/src/compile.ts b/packages/compiler-vapor/src/compile.ts index 7d07bb60d..25790d65c 100644 --- a/packages/compiler-vapor/src/compile.ts +++ b/packages/compiler-vapor/src/compile.ts @@ -27,6 +27,7 @@ import { transformVModel } from './transforms/vModel' import { transformVIf } from './transforms/vIf' import { transformVFor } from './transforms/vFor' import { transformComment } from './transforms/transformComment' +import { transformSlotOutlet } from './transforms/transformSlotOutlet' import type { HackOptions } from './ir' export { wrapTemplate } from './transforms/utils' @@ -103,6 +104,7 @@ export function getBaseTransformPreset( transformOnce, transformVIf, transformVFor, + transformSlotOutlet, transformTemplateRef, transformText, transformElement, diff --git a/packages/compiler-vapor/src/generators/operation.ts b/packages/compiler-vapor/src/generators/operation.ts index 0d1627e78..2505b3018 100644 --- a/packages/compiler-vapor/src/generators/operation.ts +++ b/packages/compiler-vapor/src/generators/operation.ts @@ -17,6 +17,7 @@ import { buildCodeFragment, } from './utils' import { genCreateComponent } from './component' +import { genSlotOutlet } from './slotOutlet' export function genOperations(opers: OperationNode[], context: CodegenContext) { const [frag, push] = buildCodeFragment() @@ -61,6 +62,8 @@ export function genOperation( return genCreateComponent(oper, context) case IRNodeTypes.DECLARE_OLD_REF: return genDeclareOldRef(oper) + case IRNodeTypes.SLOT_OUTLET_NODE: + return genSlotOutlet(oper, context) } return [] diff --git a/packages/compiler-vapor/src/generators/slotOutlet.ts b/packages/compiler-vapor/src/generators/slotOutlet.ts new file mode 100644 index 000000000..0e3c5371c --- /dev/null +++ b/packages/compiler-vapor/src/generators/slotOutlet.ts @@ -0,0 +1,34 @@ +import type { CodegenContext } from '../generate' +import type { SlotOutletIRNode } from '../ir' +import { genBlock } from './block' +import { genExpression } from './expression' +import { type CodeFragment, NEWLINE, buildCodeFragment, genCall } from './utils' +import { genRawProps } from './component' + +export function genSlotOutlet(oper: SlotOutletIRNode, context: CodegenContext) { + const { vaporHelper } = context + const { id, name, fallback } = oper + const [frag, push] = buildCodeFragment() + + const nameExpr = name.isStatic + ? genExpression(name, context) + : ['() => (', ...genExpression(name, context), ')'] + + let fallbackArg: CodeFragment[] | undefined + if (fallback) { + fallbackArg = genBlock(fallback, context) + } + + push( + NEWLINE, + `const n${id} = `, + ...genCall( + vaporHelper('createSlot'), + nameExpr, + genRawProps(oper.props, context) || 'null', + fallbackArg, + ), + ) + + return frag +} diff --git a/packages/compiler-vapor/src/index.ts b/packages/compiler-vapor/src/index.ts index 225834afc..e222fadee 100644 --- a/packages/compiler-vapor/src/index.ts +++ b/packages/compiler-vapor/src/index.ts @@ -48,3 +48,4 @@ export { transformVIf } from './transforms/vIf' export { transformVFor } from './transforms/vFor' export { transformVModel } from './transforms/vModel' export { transformComment } from './transforms/transformComment' +export { transformSlotOutlet } from './transforms/transformSlotOutlet' diff --git a/packages/compiler-vapor/src/ir.ts b/packages/compiler-vapor/src/ir.ts index 71398a7a2..bba2f3cfc 100644 --- a/packages/compiler-vapor/src/ir.ts +++ b/packages/compiler-vapor/src/ir.ts @@ -30,6 +30,7 @@ export enum IRNodeTypes { PREPEND_NODE, CREATE_TEXT_NODE, CREATE_COMPONENT_NODE, + SLOT_OUTLET_NODE, WITH_DIRECTIVE, DECLARE_OLD_REF, // consider make it more general @@ -214,6 +215,14 @@ export interface DeclareOldRefIRNode extends BaseIRNode { id: number } +export interface SlotOutletIRNode extends BaseIRNode { + type: IRNodeTypes.SLOT_OUTLET_NODE + id: number + name: SimpleExpressionNode + props: IRProps[] + fallback?: BlockIRNode +} + export type IRNode = OperationNode | RootIRNode export type OperationNode = | SetPropIRNode @@ -232,6 +241,7 @@ export type OperationNode = | ForIRNode | CreateComponentIRNode | DeclareOldRefIRNode + | SlotOutletIRNode export enum DynamicFlag { NONE = 0, diff --git a/packages/compiler-vapor/src/transforms/transformElement.ts b/packages/compiler-vapor/src/transforms/transformElement.ts index b7de58506..e45428af4 100644 --- a/packages/compiler-vapor/src/transforms/transformElement.ts +++ b/packages/compiler-vapor/src/transforms/transformElement.ts @@ -29,6 +29,7 @@ import { type IRProp, type IRProps, type IRPropsDynamicAttribute, + type IRPropsStatic, type VaporDirectiveNode, } from '../ir' import { EMPTY_EXPRESSION } from './utils' @@ -125,7 +126,7 @@ function resolveSetupReference(name: string, context: TransformContext) { function transformNativeElement( tag: string, - propsResult: ReturnType, + propsResult: PropsResult, context: TransformContext, ) { const { scopeId } = context.options @@ -179,9 +180,9 @@ function transformNativeElement( export type PropsResult = | [dynamic: true, props: IRProps[], expressions: SimpleExpressionNode[]] - | [dynamic: false, props: IRProp[]] + | [dynamic: false, props: IRPropsStatic] -function buildProps( +export function buildProps( node: ElementNode, context: TransformContext, isComponent: boolean, diff --git a/packages/compiler-vapor/src/transforms/transformSlotOutlet.ts b/packages/compiler-vapor/src/transforms/transformSlotOutlet.ts new file mode 100644 index 000000000..5db2e1fcb --- /dev/null +++ b/packages/compiler-vapor/src/transforms/transformSlotOutlet.ts @@ -0,0 +1,133 @@ +import { + type AttributeNode, + type ElementNode, + ElementTypes, + ErrorCodes, + NodeTypes, + type SimpleExpressionNode, + createCompilerError, + createSimpleExpression, + isStaticArgOf, + isStaticExp, +} from '@vue/compiler-core' +import type { NodeTransform, TransformContext } from '../transform' +import { + type BlockIRNode, + DynamicFlag, + IRNodeTypes, + type IRProps, + type VaporDirectiveNode, + type WithDirectiveIRNode, +} from '../ir' +import { camelize, extend } from '@vue/shared' +import { newBlock } from './utils' +import { buildProps } from './transformElement' + +export const transformSlotOutlet: NodeTransform = (node, context) => { + if (node.type !== NodeTypes.ELEMENT || node.tag !== 'slot') { + return + } + const id = context.reference() + context.dynamic.flags |= DynamicFlag.INSERT | DynamicFlag.NON_TEMPLATE + const [fallback, exitBlock] = createFallback( + node, + context as TransformContext, + ) + + let slotName: SimpleExpressionNode | undefined + const slotProps: (AttributeNode | VaporDirectiveNode)[] = [] + for (const prop of node.props as (AttributeNode | VaporDirectiveNode)[]) { + if (prop.type === NodeTypes.ATTRIBUTE) { + if (prop.value) { + if (prop.name === 'name') { + slotName = createSimpleExpression(prop.value.content, true, prop.loc) + } else { + slotProps.push(extend({}, prop, { name: camelize(prop.name) })) + } + } + } else if (prop.name === 'bind' && isStaticArgOf(prop.arg, 'name')) { + if (prop.exp) { + slotName = prop.exp! + } else { + // v-bind shorthand syntax + slotName = createSimpleExpression( + camelize(prop.arg!.content), + false, + prop.arg!.loc, + ) + slotName.ast = null + } + } else { + let slotProp = prop + if ( + slotProp.name === 'bind' && + slotProp.arg && + isStaticExp(slotProp.arg) + ) { + slotProp = extend({}, prop, { + arg: extend({}, slotProp.arg, { + content: camelize(slotProp.arg!.content), + }), + }) + } + slotProps.push(slotProp) + } + } + + slotName ||= createSimpleExpression('default', true) + let irProps: IRProps[] = [] + if (slotProps.length) { + const [isDynamic, props] = buildProps( + extend({}, node, { props: slotProps }), + context as TransformContext, + true, + ) + irProps = isDynamic ? props : [props] + + const runtimeDirective = context.block.operation.find( + (oper): oper is WithDirectiveIRNode => + oper.type === IRNodeTypes.WITH_DIRECTIVE && oper.element === id, + ) + if (runtimeDirective) { + context.options.onError( + createCompilerError( + ErrorCodes.X_V_SLOT_UNEXPECTED_DIRECTIVE_ON_SLOT_OUTLET, + runtimeDirective.dir.loc, + ), + ) + } + } + + return () => { + exitBlock && exitBlock() + context.registerOperation({ + type: IRNodeTypes.SLOT_OUTLET_NODE, + id, + name: slotName, + props: irProps, + fallback, + }) + } +} + +function createFallback( + node: ElementNode, + context: TransformContext, +): [block?: BlockIRNode, exit?: () => void] { + if (!node.children.length) { + return [] + } + + context.node = node = extend({}, node, { + type: NodeTypes.ELEMENT, + tag: 'template', + props: [], + tagType: ElementTypes.TEMPLATE, + children: node.children, + }) + + const fallback = newBlock(node) + const exitBlock = context.enterBlock(fallback) + context.reference() + return [fallback, exitBlock] +} diff --git a/packages/compiler-vapor/src/transforms/vBind.ts b/packages/compiler-vapor/src/transforms/vBind.ts index 3a5bb0948..3d7b4a839 100644 --- a/packages/compiler-vapor/src/transforms/vBind.ts +++ b/packages/compiler-vapor/src/transforms/vBind.ts @@ -5,7 +5,7 @@ import { createCompilerError, createSimpleExpression, } from '@vue/compiler-dom' -import { camelize } from '@vue/shared' +import { camelize, extend } from '@vue/shared' import type { DirectiveTransform, TransformContext } from '../transform' import { resolveExpression } from '../utils' import { isReservedProp } from './transformElement' @@ -58,7 +58,7 @@ export const transformVBind: DirectiveTransform = (dir, node, context) => { let camel = false if (modifiers.includes('camel')) { if (arg.isStatic) { - arg.content = camelize(arg.content) + arg = extend({}, arg, { content: camelize(arg.content) }) } else { camel = true } diff --git a/packages/compiler-vapor/src/transforms/vOn.ts b/packages/compiler-vapor/src/transforms/vOn.ts index e976a09f4..6f04a0c1b 100644 --- a/packages/compiler-vapor/src/transforms/vOn.ts +++ b/packages/compiler-vapor/src/transforms/vOn.ts @@ -20,6 +20,7 @@ const delegatedEvents = /*#__PURE__*/ makeMap( export const transformVOn: DirectiveTransform = (dir, node, context) => { let { arg, exp, loc, modifiers } = dir const isComponent = node.tagType === ElementTypes.COMPONENT + const isSlotOutlet = node.tag === 'slot' if (!exp && !modifiers.length) { context.options.onError( @@ -60,7 +61,7 @@ export const transformVOn: DirectiveTransform = (dir, node, context) => { } } - if (isComponent) { + if (isComponent || isSlotOutlet) { const handler = exp || EMPTY_EXPRESSION return { key: arg,