From 6fab797bbae49e837e3cd0469f757e1e45f99758 Mon Sep 17 00:00:00 2001 From: Rizumu Ayaka Date: Sat, 20 Apr 2024 07:34:16 +0800 Subject: [PATCH 01/16] feat(compiler-vapor): slot outlet --- .../transformSlotOutlet.spec.ts.snap | 19 ++++ .../transforms/transformSlotOutlet.spec.ts | 61 ++++++++++++ packages/compiler-vapor/src/compile.ts | 2 + .../src/generators/operation.ts | 3 + .../src/generators/slotOutlet.ts | 23 +++++ packages/compiler-vapor/src/index.ts | 1 + packages/compiler-vapor/src/ir.ts | 10 ++ .../src/transforms/transformSlotOutlet.ts | 95 +++++++++++++++++++ 8 files changed, 214 insertions(+) create mode 100644 packages/compiler-vapor/__tests__/transforms/__snapshots__/transformSlotOutlet.spec.ts.snap create mode 100644 packages/compiler-vapor/__tests__/transforms/transformSlotOutlet.spec.ts create mode 100644 packages/compiler-vapor/src/generators/slotOutlet.ts create mode 100644 packages/compiler-vapor/src/transforms/transformSlotOutlet.ts 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..71396ae44 --- /dev/null +++ b/packages/compiler-vapor/__tests__/transforms/__snapshots__/transformSlotOutlet.spec.ts.snap @@ -0,0 +1,19 @@ +// 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") + 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") + 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..abd6a3591 --- /dev/null +++ b/packages/compiler-vapor/__tests__/transforms/transformSlotOutlet.spec.ts @@ -0,0 +1,61 @@ +import { NodeTypes } from '@vue/compiler-core' +import { + IRNodeTypes, + transformChildren, + transformElement, + transformSlotOutlet, + transformText, + transformVBind, + transformVOn, +} from '../../src' +import { makeCompile } from './_utils' + +const compileWithSlotsOutlet = makeCompile({ + nodeTransforms: [ + transformText, + transformSlotOutlet, + transformElement, + transformChildren, + ], + directiveTransforms: { + bind: transformVBind, + on: transformVOn, + }, +}) + +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', + }, + }, + ]) + }) +}) 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..fe8dcb516 --- /dev/null +++ b/packages/compiler-vapor/src/generators/slotOutlet.ts @@ -0,0 +1,23 @@ +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' + +export function genSlotOutlet(oper: SlotOutletIRNode, context: CodegenContext) { + const { vaporHelper } = context + const { id, name, fallback } = oper + const [frag, push] = buildCodeFragment() + + const nameExpr = genExpression(name, context) + + let fallbackArg: false | CodeFragment[] = false + if (fallback) { + fallbackArg = genBlock(fallback, context) + } + + push(NEWLINE, `const n${id} = `) + push(...genCall(vaporHelper('createSlot'), nameExpr, false, 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/transformSlotOutlet.ts b/packages/compiler-vapor/src/transforms/transformSlotOutlet.ts new file mode 100644 index 000000000..61b635cc0 --- /dev/null +++ b/packages/compiler-vapor/src/transforms/transformSlotOutlet.ts @@ -0,0 +1,95 @@ +import { + type ElementNode, + ElementTypes, + NodeTypes, + type SimpleExpressionNode, + createSimpleExpression, +} from '@vue/compiler-core' +import type { NodeTransform, TransformContext } from '../transform' +import { + type BlockIRNode, + DynamicFlag, + type IRDynamicInfo, + IRNodeTypes, + type VaporDirectiveNode, +} from '../ir' +import { camelize, extend } from '@vue/shared' +import { genDefaultDynamic } from './utils' + +export const transformSlotOutlet: NodeTransform = (node, context) => { + if (node.type !== NodeTypes.ELEMENT || node.tag !== 'slot') { + return + } + const { props } = node + const id = context.reference() + context.dynamic.flags |= DynamicFlag.INSERT + const [fallback, exitBlock] = createFallback( + node, + context as TransformContext, + ) + + let name: SimpleExpressionNode | undefined + const nonNameProps = [] + for (const p of props) { + if (p.type === NodeTypes.ATTRIBUTE) { + if (p.value) { + if (p.name === 'name') { + name = createSimpleExpression(p.value.content, true) + break + } else { + nonNameProps.push(extend({}, p, { name: camelize(p.name) })) + } + } + } else { + if (p.name === 'name') { + name = (p as VaporDirectiveNode).exp! + } else { + nonNameProps.push(p) + } + } + } + name ||= createSimpleExpression('default', true) + + return () => { + exitBlock && exitBlock() + context.registerOperation({ + type: IRNodeTypes.SLOT_OUTLET_NODE, + id, + name, + props: [], + fallback, + }) + } +} + +function createFallback( + node: ElementNode, + context: TransformContext, +): [BlockIRNode | undefined, (() => void) | undefined] { + if (!node.children.length) { + return [undefined, undefined] + } + + context.node = node = extend({}, node, { + type: NodeTypes.ELEMENT, + tag: 'template', + props: [], + tagType: ElementTypes.TEMPLATE, + children: [...node.children], + }) + + const fallback: BlockIRNode = { + type: IRNodeTypes.BLOCK, + node, + dynamic: extend(genDefaultDynamic(), { + flags: DynamicFlag.REFERENCED, + } satisfies Partial), + effect: [], + operation: [], + returns: [], + } + + const exitBlock = context.enterBlock(fallback) + context.reference() + return [fallback, exitBlock] +} From 3d5a81ba959174956e09ee88968d06c7c4fa8af1 Mon Sep 17 00:00:00 2001 From: Rizumu Ayaka Date: Sat, 20 Apr 2024 08:41:20 +0800 Subject: [PATCH 02/16] feat: dynamically named slot outlet --- .../transformSlotOutlet.spec.ts.snap | 44 +++++++++++ .../transforms/transformSlotOutlet.spec.ts | 75 +++++++++++++++++++ .../src/generators/slotOutlet.ts | 4 +- .../src/transforms/transformSlotOutlet.ts | 24 +++++- 4 files changed, 142 insertions(+), 5 deletions(-) diff --git a/packages/compiler-vapor/__tests__/transforms/__snapshots__/transformSlotOutlet.spec.ts.snap b/packages/compiler-vapor/__tests__/transforms/__snapshots__/transformSlotOutlet.spec.ts.snap index 71396ae44..9606107ad 100644 --- a/packages/compiler-vapor/__tests__/transforms/__snapshots__/transformSlotOutlet.spec.ts.snap +++ b/packages/compiler-vapor/__tests__/transforms/__snapshots__/transformSlotOutlet.spec.ts.snap @@ -9,6 +9,50 @@ export function render(_ctx) { }" `; +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", () => { + const n2 = t0() + return n2 + }) + 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)) + 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)) + 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", () => { + const n2 = t0() + return n2 + }) + return n0 +}" +`; + exports[`compiler: transform outlets > statically named slot outlet 1`] = ` "import { createSlot as _createSlot } from 'vue/vapor'; diff --git a/packages/compiler-vapor/__tests__/transforms/transformSlotOutlet.spec.ts b/packages/compiler-vapor/__tests__/transforms/transformSlotOutlet.spec.ts index abd6a3591..2a4f8f136 100644 --- a/packages/compiler-vapor/__tests__/transforms/transformSlotOutlet.spec.ts +++ b/packages/compiler-vapor/__tests__/transforms/transformSlotOutlet.spec.ts @@ -54,6 +54,81 @@ describe('compiler: transform outlets', () => { 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 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], }, }, ]) diff --git a/packages/compiler-vapor/src/generators/slotOutlet.ts b/packages/compiler-vapor/src/generators/slotOutlet.ts index fe8dcb516..a778c2a88 100644 --- a/packages/compiler-vapor/src/generators/slotOutlet.ts +++ b/packages/compiler-vapor/src/generators/slotOutlet.ts @@ -9,7 +9,9 @@ export function genSlotOutlet(oper: SlotOutletIRNode, context: CodegenContext) { const { id, name, fallback } = oper const [frag, push] = buildCodeFragment() - const nameExpr = genExpression(name, context) + const nameExpr = name.isStatic + ? genExpression(name, context) + : ['() => (', ...genExpression(name, context), ')'] let fallbackArg: false | CodeFragment[] = false if (fallback) { diff --git a/packages/compiler-vapor/src/transforms/transformSlotOutlet.ts b/packages/compiler-vapor/src/transforms/transformSlotOutlet.ts index 61b635cc0..4eb15cef2 100644 --- a/packages/compiler-vapor/src/transforms/transformSlotOutlet.ts +++ b/packages/compiler-vapor/src/transforms/transformSlotOutlet.ts @@ -4,6 +4,8 @@ import { NodeTypes, type SimpleExpressionNode, createSimpleExpression, + isStaticArgOf, + isStaticExp, } from '@vue/compiler-core' import type { NodeTransform, TransformContext } from '../transform' import { @@ -34,16 +36,30 @@ export const transformSlotOutlet: NodeTransform = (node, context) => { if (p.type === NodeTypes.ATTRIBUTE) { if (p.value) { if (p.name === 'name') { - name = createSimpleExpression(p.value.content, true) + name = createSimpleExpression(p.value.content, true, p.loc) break } else { - nonNameProps.push(extend({}, p, { name: camelize(p.name) })) + p.name = camelize(p.name) + nonNameProps.push(p) } } } else { - if (p.name === 'name') { - name = (p as VaporDirectiveNode).exp! + if (p.name === 'bind' && isStaticArgOf(p.arg, 'name')) { + if (p.exp) { + name = (p as VaporDirectiveNode).exp! + } else if (p.arg && p.arg.type === NodeTypes.SIMPLE_EXPRESSION) { + // v-bind shorthand syntax + name = createSimpleExpression( + camelize(p.arg.content), + false, + p.arg.loc, + ) + name.ast = null + } } else { + if (p.name === 'bind' && p.arg && isStaticExp(p.arg)) { + p.arg.content = camelize(p.arg.content) + } nonNameProps.push(p) } } From f14a3d4af0c21871aeb4969c46aaba63ccdac685 Mon Sep 17 00:00:00 2001 From: Rizumu Ayaka Date: Sat, 20 Apr 2024 22:02:01 +0800 Subject: [PATCH 03/16] feat: props for slot outlet --- .../transformSlotOutlet.spec.ts.snap | 52 +++++++++++++ .../transforms/transformSlotOutlet.spec.ts | 74 +++++++++++++++++++ .../src/generators/slotOutlet.ts | 64 +++++++++++++++- .../src/transforms/transformElement.ts | 2 +- .../src/transforms/transformSlotOutlet.ts | 14 +++- packages/compiler-vapor/src/transforms/vOn.ts | 3 +- 6 files changed, 201 insertions(+), 8 deletions(-) diff --git a/packages/compiler-vapor/__tests__/transforms/__snapshots__/transformSlotOutlet.spec.ts.snap b/packages/compiler-vapor/__tests__/transforms/__snapshots__/transformSlotOutlet.spec.ts.snap index 9606107ad..9bde550c0 100644 --- a/packages/compiler-vapor/__tests__/transforms/__snapshots__/transformSlotOutlet.spec.ts.snap +++ b/packages/compiler-vapor/__tests__/transforms/__snapshots__/transformSlotOutlet.spec.ts.snap @@ -22,6 +22,19 @@ export function render(_ctx) { }" `; +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'; @@ -61,3 +74,42 @@ export function render(_ctx) { 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 { toHandlers as _toHandlers } from 'vue'; +import { createSlot as _createSlot } 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 index 2a4f8f136..18209754f 100644 --- a/packages/compiler-vapor/__tests__/transforms/transformSlotOutlet.spec.ts +++ b/packages/compiler-vapor/__tests__/transforms/transformSlotOutlet.spec.ts @@ -92,6 +92,80 @@ describe('compiler: transform outlets', () => { ]) }) + 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() diff --git a/packages/compiler-vapor/src/generators/slotOutlet.ts b/packages/compiler-vapor/src/generators/slotOutlet.ts index a778c2a88..9b00f21d0 100644 --- a/packages/compiler-vapor/src/generators/slotOutlet.ts +++ b/packages/compiler-vapor/src/generators/slotOutlet.ts @@ -1,11 +1,22 @@ +import { isArray } from '@vue/shared' import type { CodegenContext } from '../generate' -import type { SlotOutletIRNode } from '../ir' +import type { IRProp, SlotOutletIRNode } from '../ir' import { genBlock } from './block' import { genExpression } from './expression' -import { type CodeFragment, NEWLINE, buildCodeFragment, genCall } from './utils' +import { + type CodeFragment, + INDENT_END, + INDENT_START, + NEWLINE, + buildCodeFragment, + genCall, + genMulti, +} from './utils' +import { genPropKey } from './prop' +import { genEventHandler } from './event' export function genSlotOutlet(oper: SlotOutletIRNode, context: CodegenContext) { - const { vaporHelper } = context + const { helper, vaporHelper } = context const { id, name, fallback } = oper const [frag, push] = buildCodeFragment() @@ -19,7 +30,52 @@ export function genSlotOutlet(oper: SlotOutletIRNode, context: CodegenContext) { } push(NEWLINE, `const n${id} = `) - push(...genCall(vaporHelper('createSlot'), nameExpr, false, fallbackArg)) + push( + ...genCall( + vaporHelper('createSlot'), + nameExpr, + genRawProps() || false, + fallbackArg, + ), + ) return frag + + // TODO share this with genCreateComponent + function genRawProps() { + const props = oper.props + .map(props => { + if (isArray(props)) { + if (!props.length) return + return genStaticProps(props) + } else { + let expr = genExpression(props.value, context) + if (props.handler) expr = genCall(helper('toHandlers'), expr) + return ['() => (', ...expr, ')'] + } + }) + .filter(Boolean) + if (props.length) { + return genMulti(['[', ']', ', '], ...props) + } + } + + function genStaticProps(props: IRProp[]) { + return genMulti( + [ + ['{', INDENT_START, NEWLINE], + [INDENT_END, NEWLINE, '}'], + [', ', NEWLINE], + ], + ...props.map(prop => { + return [ + ...genPropKey(prop, context), + ': ', + ...(prop.handler + ? genEventHandler(context, prop.values[0]) + : ['() => (', ...genExpression(prop.values[0], context), ')']), + ] + }), + ) + } } diff --git a/packages/compiler-vapor/src/transforms/transformElement.ts b/packages/compiler-vapor/src/transforms/transformElement.ts index b7de58506..d0e75784d 100644 --- a/packages/compiler-vapor/src/transforms/transformElement.ts +++ b/packages/compiler-vapor/src/transforms/transformElement.ts @@ -181,7 +181,7 @@ export type PropsResult = | [dynamic: true, props: IRProps[], expressions: SimpleExpressionNode[]] | [dynamic: false, props: IRProp[]] -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 index 4eb15cef2..36ac54787 100644 --- a/packages/compiler-vapor/src/transforms/transformSlotOutlet.ts +++ b/packages/compiler-vapor/src/transforms/transformSlotOutlet.ts @@ -13,10 +13,12 @@ import { DynamicFlag, type IRDynamicInfo, IRNodeTypes, + type IRProps, type VaporDirectiveNode, } from '../ir' import { camelize, extend } from '@vue/shared' import { genDefaultDynamic } from './utils' +import { buildProps } from './transformElement' export const transformSlotOutlet: NodeTransform = (node, context) => { if (node.type !== NodeTypes.ELEMENT || node.tag !== 'slot') { @@ -37,7 +39,6 @@ export const transformSlotOutlet: NodeTransform = (node, context) => { if (p.value) { if (p.name === 'name') { name = createSimpleExpression(p.value.content, true, p.loc) - break } else { p.name = camelize(p.name) nonNameProps.push(p) @@ -65,6 +66,15 @@ export const transformSlotOutlet: NodeTransform = (node, context) => { } } name ||= createSimpleExpression('default', true) + let irProps: IRProps[] = [] + if (nonNameProps.length > 0) { + const [isDynamic, props] = buildProps( + extend({}, node, { props: nonNameProps }), + context as TransformContext, + true, + ) + irProps = isDynamic ? props : [props] + } return () => { exitBlock && exitBlock() @@ -72,7 +82,7 @@ export const transformSlotOutlet: NodeTransform = (node, context) => { type: IRNodeTypes.SLOT_OUTLET_NODE, id, name, - props: [], + props: irProps, fallback, }) } 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, From 517f2e3a29e25689bbfac04b7c3f3ea4cf69bf89 Mon Sep 17 00:00:00 2001 From: Rizumu Ayaka Date: Sun, 21 Apr 2024 12:30:18 +0800 Subject: [PATCH 04/16] fix: wrong null value --- .../__snapshots__/transformSlotOutlet.spec.ts.snap | 12 ++++++------ .../__tests__/transforms/transformSlotOutlet.spec.ts | 2 ++ packages/compiler-vapor/src/generators/slotOutlet.ts | 2 +- 3 files changed, 9 insertions(+), 7 deletions(-) diff --git a/packages/compiler-vapor/__tests__/transforms/__snapshots__/transformSlotOutlet.spec.ts.snap b/packages/compiler-vapor/__tests__/transforms/__snapshots__/transformSlotOutlet.spec.ts.snap index 9bde550c0..2b501ddaa 100644 --- a/packages/compiler-vapor/__tests__/transforms/__snapshots__/transformSlotOutlet.spec.ts.snap +++ b/packages/compiler-vapor/__tests__/transforms/__snapshots__/transformSlotOutlet.spec.ts.snap @@ -4,7 +4,7 @@ exports[`compiler: transform outlets > default slot outlet 1`] = ` "import { createSlot as _createSlot } from 'vue/vapor'; export function render(_ctx) { - const n0 = _createSlot("default") + const n0 = _createSlot("default", null) return n0 }" `; @@ -14,7 +14,7 @@ exports[`compiler: transform outlets > default slot outlet with fallback const t0 = _template("
") export function render(_ctx) { - const n0 = _createSlot("default", () => { + const n0 = _createSlot("default", null, () => { const n2 = t0() return n2 }) @@ -39,7 +39,7 @@ 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)) + const n0 = _createSlot(() => (_ctx.foo + _ctx.bar), null) return n0 }" `; @@ -48,7 +48,7 @@ exports[`compiler: transform outlets > dynamically named slot outlet with "import { createSlot as _createSlot } from 'vue/vapor'; export function render(_ctx) { - const n0 = _createSlot(() => (_ctx.name)) + const n0 = _createSlot(() => (_ctx.name), null) return n0 }" `; @@ -58,7 +58,7 @@ exports[`compiler: transform outlets > named slot outlet with fallback 1` const t0 = _template("
") export function render(_ctx) { - const n0 = _createSlot("foo", () => { + const n0 = _createSlot("foo", null, () => { const n2 = t0() return n2 }) @@ -70,7 +70,7 @@ exports[`compiler: transform outlets > statically named slot outlet 1`] = "import { createSlot as _createSlot } from 'vue/vapor'; export function render(_ctx) { - const n0 = _createSlot("foo") + const n0 = _createSlot("foo", null) return n0 }" `; diff --git a/packages/compiler-vapor/__tests__/transforms/transformSlotOutlet.spec.ts b/packages/compiler-vapor/__tests__/transforms/transformSlotOutlet.spec.ts index 18209754f..3aa9eb3cc 100644 --- a/packages/compiler-vapor/__tests__/transforms/transformSlotOutlet.spec.ts +++ b/packages/compiler-vapor/__tests__/transforms/transformSlotOutlet.spec.ts @@ -207,4 +207,6 @@ describe('compiler: transform outlets', () => { }, ]) }) + + test.todo('error on unexpected custom directive on ') }) diff --git a/packages/compiler-vapor/src/generators/slotOutlet.ts b/packages/compiler-vapor/src/generators/slotOutlet.ts index 9b00f21d0..1f4000fc1 100644 --- a/packages/compiler-vapor/src/generators/slotOutlet.ts +++ b/packages/compiler-vapor/src/generators/slotOutlet.ts @@ -34,7 +34,7 @@ export function genSlotOutlet(oper: SlotOutletIRNode, context: CodegenContext) { ...genCall( vaporHelper('createSlot'), nameExpr, - genRawProps() || false, + genRawProps() || 'null', fallbackArg, ), ) From 2b034fa8761d9d57c18fc4eed5ffd8817844a690 Mon Sep 17 00:00:00 2001 From: Rizumu Ayaka Date: Sun, 21 Apr 2024 13:21:26 +0800 Subject: [PATCH 05/16] feat: error on unexpected custom directive on --- .../transformSlotOutlet.spec.ts.snap | 9 +++++ .../transforms/transformSlotOutlet.spec.ts | 25 +++++++++++-- .../src/transforms/transformSlotOutlet.ts | 36 +++++++++++++------ 3 files changed, 58 insertions(+), 12 deletions(-) diff --git a/packages/compiler-vapor/__tests__/transforms/__snapshots__/transformSlotOutlet.spec.ts.snap b/packages/compiler-vapor/__tests__/transforms/__snapshots__/transformSlotOutlet.spec.ts.snap index 2b501ddaa..c88af0aab 100644 --- a/packages/compiler-vapor/__tests__/transforms/__snapshots__/transformSlotOutlet.spec.ts.snap +++ b/packages/compiler-vapor/__tests__/transforms/__snapshots__/transformSlotOutlet.spec.ts.snap @@ -53,6 +53,15 @@ export function render(_ctx) { }" `; +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 > named slot outlet with fallback 1`] = ` "import { createSlot as _createSlot, template as _template } from 'vue/vapor'; const t0 = _template("
") diff --git a/packages/compiler-vapor/__tests__/transforms/transformSlotOutlet.spec.ts b/packages/compiler-vapor/__tests__/transforms/transformSlotOutlet.spec.ts index 3aa9eb3cc..fc68e18ec 100644 --- a/packages/compiler-vapor/__tests__/transforms/transformSlotOutlet.spec.ts +++ b/packages/compiler-vapor/__tests__/transforms/transformSlotOutlet.spec.ts @@ -1,4 +1,4 @@ -import { NodeTypes } from '@vue/compiler-core' +import { ErrorCodes, NodeTypes } from '@vue/compiler-core' import { IRNodeTypes, transformChildren, @@ -208,5 +208,26 @@ describe('compiler: transform outlets', () => { ]) }) - test.todo('error on unexpected custom directive on ') + 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, + }, + }, + }) + }) }) diff --git a/packages/compiler-vapor/src/transforms/transformSlotOutlet.ts b/packages/compiler-vapor/src/transforms/transformSlotOutlet.ts index 36ac54787..8f03509d6 100644 --- a/packages/compiler-vapor/src/transforms/transformSlotOutlet.ts +++ b/packages/compiler-vapor/src/transforms/transformSlotOutlet.ts @@ -1,8 +1,12 @@ import { + type AttributeNode, + type DirectiveNode, type ElementNode, ElementTypes, + ErrorCodes, NodeTypes, type SimpleExpressionNode, + createCompilerError, createSimpleExpression, isStaticArgOf, isStaticExp, @@ -11,13 +15,12 @@ import type { NodeTransform, TransformContext } from '../transform' import { type BlockIRNode, DynamicFlag, - type IRDynamicInfo, IRNodeTypes, type IRProps, type VaporDirectiveNode, } from '../ir' -import { camelize, extend } from '@vue/shared' -import { genDefaultDynamic } from './utils' +import { camelize, extend, isBuiltInDirective } from '@vue/shared' +import { newDynamic } from './utils' import { buildProps } from './transformElement' export const transformSlotOutlet: NodeTransform = (node, context) => { @@ -33,7 +36,8 @@ export const transformSlotOutlet: NodeTransform = (node, context) => { ) let name: SimpleExpressionNode | undefined - const nonNameProps = [] + const nonNameProps: (AttributeNode | DirectiveNode)[] = [] + const customDirectives: DirectiveNode[] = [] for (const p of props) { if (p.type === NodeTypes.ATTRIBUTE) { if (p.value) { @@ -58,13 +62,27 @@ export const transformSlotOutlet: NodeTransform = (node, context) => { name.ast = null } } else { - if (p.name === 'bind' && p.arg && isStaticExp(p.arg)) { - p.arg.content = camelize(p.arg.content) + if (!isBuiltInDirective(p.name)) { + customDirectives.push(p) + } else { + if (p.name === 'bind' && p.arg && isStaticExp(p.arg)) { + p.arg.content = camelize(p.arg.content) + } + nonNameProps.push(p) } - nonNameProps.push(p) } } } + + if (customDirectives.length) { + context.options.onError( + createCompilerError( + ErrorCodes.X_V_SLOT_UNEXPECTED_DIRECTIVE_ON_SLOT_OUTLET, + customDirectives[0].loc, + ), + ) + } + name ||= createSimpleExpression('default', true) let irProps: IRProps[] = [] if (nonNameProps.length > 0) { @@ -107,9 +125,7 @@ function createFallback( const fallback: BlockIRNode = { type: IRNodeTypes.BLOCK, node, - dynamic: extend(genDefaultDynamic(), { - flags: DynamicFlag.REFERENCED, - } satisfies Partial), + dynamic: newDynamic(), effect: [], operation: [], returns: [], From f34a9903018fba7f2ff00c0c8a9564241f021514 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=89=E5=92=B2=E6=99=BA=E5=AD=90=20Kevin=20Deng?= Date: Sun, 28 Apr 2024 03:41:43 +0900 Subject: [PATCH 06/16] refactor: improve --- .../compiler-vapor/src/generators/slotOutlet.ts | 14 +++++--------- 1 file changed, 5 insertions(+), 9 deletions(-) diff --git a/packages/compiler-vapor/src/generators/slotOutlet.ts b/packages/compiler-vapor/src/generators/slotOutlet.ts index 1f4000fc1..e2b99c595 100644 --- a/packages/compiler-vapor/src/generators/slotOutlet.ts +++ b/packages/compiler-vapor/src/generators/slotOutlet.ts @@ -5,9 +5,9 @@ import { genBlock } from './block' import { genExpression } from './expression' import { type CodeFragment, - INDENT_END, - INDENT_START, NEWLINE, + SEGMENTS_ARRAY, + SEGMENTS_OBJECT_NEWLINE, buildCodeFragment, genCall, genMulti, @@ -24,7 +24,7 @@ export function genSlotOutlet(oper: SlotOutletIRNode, context: CodegenContext) { ? genExpression(name, context) : ['() => (', ...genExpression(name, context), ')'] - let fallbackArg: false | CodeFragment[] = false + let fallbackArg: CodeFragment[] | undefined if (fallback) { fallbackArg = genBlock(fallback, context) } @@ -56,17 +56,13 @@ export function genSlotOutlet(oper: SlotOutletIRNode, context: CodegenContext) { }) .filter(Boolean) if (props.length) { - return genMulti(['[', ']', ', '], ...props) + return genMulti(SEGMENTS_ARRAY, ...props) } } function genStaticProps(props: IRProp[]) { return genMulti( - [ - ['{', INDENT_START, NEWLINE], - [INDENT_END, NEWLINE, '}'], - [', ', NEWLINE], - ], + SEGMENTS_OBJECT_NEWLINE, ...props.map(prop => { return [ ...genPropKey(prop, context), From 033549f40758c501d40848a7bca1e9d13fcd23b7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=89=E5=92=B2=E6=99=BA=E5=AD=90=20Kevin=20Deng?= Date: Sun, 28 Apr 2024 03:48:30 +0900 Subject: [PATCH 07/16] refactor(compiler-vapor): dedupe gen props --- .../src/generators/slotOutlet.ts | 54 ++----------------- 1 file changed, 5 insertions(+), 49 deletions(-) diff --git a/packages/compiler-vapor/src/generators/slotOutlet.ts b/packages/compiler-vapor/src/generators/slotOutlet.ts index e2b99c595..713de0206 100644 --- a/packages/compiler-vapor/src/generators/slotOutlet.ts +++ b/packages/compiler-vapor/src/generators/slotOutlet.ts @@ -1,22 +1,12 @@ -import { isArray } from '@vue/shared' import type { CodegenContext } from '../generate' -import type { IRProp, SlotOutletIRNode } from '../ir' +import type { SlotOutletIRNode } from '../ir' import { genBlock } from './block' import { genExpression } from './expression' -import { - type CodeFragment, - NEWLINE, - SEGMENTS_ARRAY, - SEGMENTS_OBJECT_NEWLINE, - buildCodeFragment, - genCall, - genMulti, -} from './utils' -import { genPropKey } from './prop' -import { genEventHandler } from './event' +import { type CodeFragment, NEWLINE, buildCodeFragment, genCall } from './utils' +import { genRawProps } from './component' export function genSlotOutlet(oper: SlotOutletIRNode, context: CodegenContext) { - const { helper, vaporHelper } = context + const { vaporHelper } = context const { id, name, fallback } = oper const [frag, push] = buildCodeFragment() @@ -34,44 +24,10 @@ export function genSlotOutlet(oper: SlotOutletIRNode, context: CodegenContext) { ...genCall( vaporHelper('createSlot'), nameExpr, - genRawProps() || 'null', + genRawProps(oper.props, context) || 'null', fallbackArg, ), ) return frag - - // TODO share this with genCreateComponent - function genRawProps() { - const props = oper.props - .map(props => { - if (isArray(props)) { - if (!props.length) return - return genStaticProps(props) - } else { - let expr = genExpression(props.value, context) - if (props.handler) expr = genCall(helper('toHandlers'), expr) - return ['() => (', ...expr, ')'] - } - }) - .filter(Boolean) - if (props.length) { - return genMulti(SEGMENTS_ARRAY, ...props) - } - } - - function genStaticProps(props: IRProp[]) { - return genMulti( - SEGMENTS_OBJECT_NEWLINE, - ...props.map(prop => { - return [ - ...genPropKey(prop, context), - ': ', - ...(prop.handler - ? genEventHandler(context, prop.values[0]) - : ['() => (', ...genExpression(prop.values[0], context), ')']), - ] - }), - ) - } } From d117fa7fb1ab29538e9edd28d1a28652b1642d13 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=89=E5=92=B2=E6=99=BA=E5=AD=90=20Kevin=20Deng?= Date: Sun, 28 Apr 2024 23:08:16 +0900 Subject: [PATCH 08/16] refactor(compiler-vapor): extract new block --- .../src/transforms/transformSlotOutlet.ts | 12 ++---------- 1 file changed, 2 insertions(+), 10 deletions(-) diff --git a/packages/compiler-vapor/src/transforms/transformSlotOutlet.ts b/packages/compiler-vapor/src/transforms/transformSlotOutlet.ts index 8f03509d6..809c371e7 100644 --- a/packages/compiler-vapor/src/transforms/transformSlotOutlet.ts +++ b/packages/compiler-vapor/src/transforms/transformSlotOutlet.ts @@ -20,7 +20,7 @@ import { type VaporDirectiveNode, } from '../ir' import { camelize, extend, isBuiltInDirective } from '@vue/shared' -import { newDynamic } from './utils' +import { newBlock } from './utils' import { buildProps } from './transformElement' export const transformSlotOutlet: NodeTransform = (node, context) => { @@ -122,15 +122,7 @@ function createFallback( children: [...node.children], }) - const fallback: BlockIRNode = { - type: IRNodeTypes.BLOCK, - node, - dynamic: newDynamic(), - effect: [], - operation: [], - returns: [], - } - + const fallback: BlockIRNode = newBlock(node) const exitBlock = context.enterBlock(fallback) context.reference() return [fallback, exitBlock] From 2417f3a5c48f06e9cc61727ab107d2a211429918 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=89=E5=92=B2=E6=99=BA=E5=AD=90=20Kevin=20Deng?= Date: Sun, 28 Apr 2024 23:32:19 +0900 Subject: [PATCH 09/16] refactor --- .../src/transforms/transformSlotOutlet.ts | 62 +++++++++---------- 1 file changed, 29 insertions(+), 33 deletions(-) diff --git a/packages/compiler-vapor/src/transforms/transformSlotOutlet.ts b/packages/compiler-vapor/src/transforms/transformSlotOutlet.ts index 809c371e7..07e1d50dd 100644 --- a/packages/compiler-vapor/src/transforms/transformSlotOutlet.ts +++ b/packages/compiler-vapor/src/transforms/transformSlotOutlet.ts @@ -37,48 +37,44 @@ export const transformSlotOutlet: NodeTransform = (node, context) => { let name: SimpleExpressionNode | undefined const nonNameProps: (AttributeNode | DirectiveNode)[] = [] - const customDirectives: DirectiveNode[] = [] - for (const p of props) { - if (p.type === NodeTypes.ATTRIBUTE) { - if (p.value) { - if (p.name === 'name') { - name = createSimpleExpression(p.value.content, true, p.loc) + const directives: DirectiveNode[] = [] + for (const prop of props) { + if (prop.type === NodeTypes.ATTRIBUTE) { + if (prop.value) { + if (prop.name === 'name') { + name = createSimpleExpression(prop.value.content, true, prop.loc) } else { - p.name = camelize(p.name) - nonNameProps.push(p) + prop.name = camelize(prop.name) + nonNameProps.push(prop) } } + } else if (prop.name === 'bind' && isStaticArgOf(prop.arg, 'name')) { + if (prop.exp) { + name = (prop as VaporDirectiveNode).exp! + } else if (prop.arg && prop.arg.type === NodeTypes.SIMPLE_EXPRESSION) { + // v-bind shorthand syntax + name = createSimpleExpression( + camelize(prop.arg.content), + false, + prop.arg.loc, + ) + name.ast = null + } + } else if (!isBuiltInDirective(prop.name)) { + directives.push(prop) } else { - if (p.name === 'bind' && isStaticArgOf(p.arg, 'name')) { - if (p.exp) { - name = (p as VaporDirectiveNode).exp! - } else if (p.arg && p.arg.type === NodeTypes.SIMPLE_EXPRESSION) { - // v-bind shorthand syntax - name = createSimpleExpression( - camelize(p.arg.content), - false, - p.arg.loc, - ) - name.ast = null - } - } else { - if (!isBuiltInDirective(p.name)) { - customDirectives.push(p) - } else { - if (p.name === 'bind' && p.arg && isStaticExp(p.arg)) { - p.arg.content = camelize(p.arg.content) - } - nonNameProps.push(p) - } + if (prop.name === 'bind' && prop.arg && isStaticExp(prop.arg)) { + prop.arg.content = camelize(prop.arg.content) } + nonNameProps.push(prop) } } - if (customDirectives.length) { + if (directives.length) { context.options.onError( createCompilerError( ErrorCodes.X_V_SLOT_UNEXPECTED_DIRECTIVE_ON_SLOT_OUTLET, - customDirectives[0].loc, + directives[0].loc, ), ) } @@ -109,9 +105,9 @@ export const transformSlotOutlet: NodeTransform = (node, context) => { function createFallback( node: ElementNode, context: TransformContext, -): [BlockIRNode | undefined, (() => void) | undefined] { +): [block?: BlockIRNode, exit?: () => void] { if (!node.children.length) { - return [undefined, undefined] + return [] } context.node = node = extend({}, node, { From 610c818068b11e85563f47cfc1225c1e1ce1e35c Mon Sep 17 00:00:00 2001 From: Rizumu Ayaka Date: Mon, 29 Apr 2024 18:39:10 +0800 Subject: [PATCH 10/16] fix: AST should not be mutated --- .../src/transforms/transformSlotOutlet.ts | 12 +++++++----- packages/compiler-vapor/src/transforms/vBind.ts | 4 ++-- 2 files changed, 9 insertions(+), 7 deletions(-) diff --git a/packages/compiler-vapor/src/transforms/transformSlotOutlet.ts b/packages/compiler-vapor/src/transforms/transformSlotOutlet.ts index 07e1d50dd..16801363e 100644 --- a/packages/compiler-vapor/src/transforms/transformSlotOutlet.ts +++ b/packages/compiler-vapor/src/transforms/transformSlotOutlet.ts @@ -44,8 +44,7 @@ export const transformSlotOutlet: NodeTransform = (node, context) => { if (prop.name === 'name') { name = createSimpleExpression(prop.value.content, true, prop.loc) } else { - prop.name = camelize(prop.name) - nonNameProps.push(prop) + nonNameProps.push(extend({}, prop, { name: camelize(prop.name) })) } } } else if (prop.name === 'bind' && isStaticArgOf(prop.arg, 'name')) { @@ -63,10 +62,13 @@ export const transformSlotOutlet: NodeTransform = (node, context) => { } else if (!isBuiltInDirective(prop.name)) { directives.push(prop) } else { - if (prop.name === 'bind' && prop.arg && isStaticExp(prop.arg)) { - prop.arg.content = camelize(prop.arg.content) + const nonProp = extend({}, prop) + if (nonProp.name === 'bind' && nonProp.arg && isStaticExp(nonProp.arg)) { + nonProp.arg = extend({}, nonProp.arg, { + content: camelize(nonProp.arg.content), + }) } - nonNameProps.push(prop) + nonNameProps.push(nonProp) } } 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 } From 1e4d9a4853fce5af397a93bda0e7c1893fb11e80 Mon Sep 17 00:00:00 2001 From: Rizumu Ayaka Date: Wed, 1 May 2024 15:55:52 +0800 Subject: [PATCH 11/16] fix: error with v-show --- .../transformSlotOutlet.spec.ts.snap | 51 ++++++++++++------- .../transforms/transformSlotOutlet.spec.ts | 25 +++++++++ .../src/transforms/transformSlotOutlet.ts | 29 ++++++----- 3 files changed, 73 insertions(+), 32 deletions(-) diff --git a/packages/compiler-vapor/__tests__/transforms/__snapshots__/transformSlotOutlet.spec.ts.snap b/packages/compiler-vapor/__tests__/transforms/__snapshots__/transformSlotOutlet.spec.ts.snap index c88af0aab..ebeaaef7f 100644 --- a/packages/compiler-vapor/__tests__/transforms/__snapshots__/transformSlotOutlet.spec.ts.snap +++ b/packages/compiler-vapor/__tests__/transforms/__snapshots__/transformSlotOutlet.spec.ts.snap @@ -26,11 +26,13 @@ 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) - }]) + const n0 = _createSlot("default", [ + { + foo: () => ("bar"), + baz: () => (_ctx.qux), + fooBar: () => (_ctx.foo-_ctx.bar) + } + ]) return n0 }" `; @@ -62,6 +64,15 @@ export function render(_ctx) { }" `; +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("
") @@ -88,10 +99,12 @@ exports[`compiler: transform outlets > statically named slot outlet with "import { createSlot as _createSlot } from 'vue/vapor'; export function render(_ctx) { - const n0 = _createSlot("foo", [{ - foo: () => ("bar"), - baz: () => (_ctx.qux) - }]) + const n0 = _createSlot("foo", [ + { + foo: () => ("bar"), + baz: () => (_ctx.qux) + } + ]) return n0 }" `; @@ -100,11 +113,11 @@ exports[`compiler: transform outlets > statically named slot outlet with "import { createSlot as _createSlot } from 'vue/vapor'; export function render(_ctx) { - const n0 = _createSlot("foo", [{ - foo: () => ("bar") - }, () => (_ctx.obj), { - baz: () => (_ctx.qux) - }]) + const n0 = _createSlot("foo", [ + { foo: () => ("bar") }, + () => (_ctx.obj), + { baz: () => (_ctx.qux) } + ]) return n0 }" `; @@ -114,11 +127,11 @@ exports[`compiler: transform outlets > statically named slot outlet with import { createSlot as _createSlot } from 'vue/vapor'; export function render(_ctx) { - const n0 = _createSlot("default", [{ - onClick: () => _ctx.foo - }, () => (_toHandlers(_ctx.bar)), { - baz: () => (_ctx.qux) - }]) + 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 index fc68e18ec..aee60e5b3 100644 --- a/packages/compiler-vapor/__tests__/transforms/transformSlotOutlet.spec.ts +++ b/packages/compiler-vapor/__tests__/transforms/transformSlotOutlet.spec.ts @@ -7,6 +7,7 @@ import { transformText, transformVBind, transformVOn, + transformVShow, } from '../../src' import { makeCompile } from './_utils' @@ -20,6 +21,7 @@ const compileWithSlotsOutlet = makeCompile({ directiveTransforms: { bind: transformVBind, on: transformVOn, + show: transformVShow, }, }) @@ -230,4 +232,27 @@ describe('compiler: transform outlets', () => { }, }) }) + + 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/transforms/transformSlotOutlet.ts b/packages/compiler-vapor/src/transforms/transformSlotOutlet.ts index 16801363e..52b24e940 100644 --- a/packages/compiler-vapor/src/transforms/transformSlotOutlet.ts +++ b/packages/compiler-vapor/src/transforms/transformSlotOutlet.ts @@ -18,8 +18,9 @@ import { IRNodeTypes, type IRProps, type VaporDirectiveNode, + type WithDirectiveIRNode, } from '../ir' -import { camelize, extend, isBuiltInDirective } from '@vue/shared' +import { camelize, extend } from '@vue/shared' import { newBlock } from './utils' import { buildProps } from './transformElement' @@ -37,7 +38,6 @@ export const transformSlotOutlet: NodeTransform = (node, context) => { let name: SimpleExpressionNode | undefined const nonNameProps: (AttributeNode | DirectiveNode)[] = [] - const directives: DirectiveNode[] = [] for (const prop of props) { if (prop.type === NodeTypes.ATTRIBUTE) { if (prop.value) { @@ -59,8 +59,6 @@ export const transformSlotOutlet: NodeTransform = (node, context) => { ) name.ast = null } - } else if (!isBuiltInDirective(prop.name)) { - directives.push(prop) } else { const nonProp = extend({}, prop) if (nonProp.name === 'bind' && nonProp.arg && isStaticExp(nonProp.arg)) { @@ -72,15 +70,6 @@ export const transformSlotOutlet: NodeTransform = (node, context) => { } } - if (directives.length) { - context.options.onError( - createCompilerError( - ErrorCodes.X_V_SLOT_UNEXPECTED_DIRECTIVE_ON_SLOT_OUTLET, - directives[0].loc, - ), - ) - } - name ||= createSimpleExpression('default', true) let irProps: IRProps[] = [] if (nonNameProps.length > 0) { @@ -90,6 +79,20 @@ export const transformSlotOutlet: NodeTransform = (node, context) => { true, ) irProps = isDynamic ? props : [props] + + const { operation } = context.block + const directives = operation.filter( + oper => oper.type === IRNodeTypes.WITH_DIRECTIVE, + ) as WithDirectiveIRNode[] + + if (directives.length) { + context.options.onError( + createCompilerError( + ErrorCodes.X_V_SLOT_UNEXPECTED_DIRECTIVE_ON_SLOT_OUTLET, + directives[0].dir.loc, + ), + ) + } } return () => { From 1609cda181ebab7171693e4edf8ee36436cc2796 Mon Sep 17 00:00:00 2001 From: Rizumu Ayaka Date: Thu, 2 May 2024 18:39:06 +0800 Subject: [PATCH 12/16] chore: rename var --- .../compiler-vapor/src/transforms/transformSlotOutlet.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/compiler-vapor/src/transforms/transformSlotOutlet.ts b/packages/compiler-vapor/src/transforms/transformSlotOutlet.ts index 52b24e940..cda48582e 100644 --- a/packages/compiler-vapor/src/transforms/transformSlotOutlet.ts +++ b/packages/compiler-vapor/src/transforms/transformSlotOutlet.ts @@ -81,15 +81,15 @@ export const transformSlotOutlet: NodeTransform = (node, context) => { irProps = isDynamic ? props : [props] const { operation } = context.block - const directives = operation.filter( + const hasDirectives = operation.filter( oper => oper.type === IRNodeTypes.WITH_DIRECTIVE, ) as WithDirectiveIRNode[] - if (directives.length) { + if (hasDirectives.length) { context.options.onError( createCompilerError( ErrorCodes.X_V_SLOT_UNEXPECTED_DIRECTIVE_ON_SLOT_OUTLET, - directives[0].dir.loc, + hasDirectives[0].dir.loc, ), ) } From c4f6d0f6ac1bb42cf4952ee6632ba372c9f33793 Mon Sep 17 00:00:00 2001 From: Rizumu Ayaka Date: Thu, 2 May 2024 21:29:52 +0800 Subject: [PATCH 13/16] ci: update spec file --- .../transforms/__snapshots__/transformSlotOutlet.spec.ts.snap | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/packages/compiler-vapor/__tests__/transforms/__snapshots__/transformSlotOutlet.spec.ts.snap b/packages/compiler-vapor/__tests__/transforms/__snapshots__/transformSlotOutlet.spec.ts.snap index ebeaaef7f..72b9b95f1 100644 --- a/packages/compiler-vapor/__tests__/transforms/__snapshots__/transformSlotOutlet.spec.ts.snap +++ b/packages/compiler-vapor/__tests__/transforms/__snapshots__/transformSlotOutlet.spec.ts.snap @@ -123,8 +123,7 @@ export function render(_ctx) { `; exports[`compiler: transform outlets > statically named slot outlet with v-on 1`] = ` -"import { toHandlers as _toHandlers } from 'vue'; -import { createSlot as _createSlot } from 'vue/vapor'; +"import { createSlot as _createSlot, toHandlers as _toHandlers } from 'vue/vapor'; export function render(_ctx) { const n0 = _createSlot("default", [ From 52c4cf09052fb9890f277091582d21f9b5dcde9e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=89=E5=92=B2=E6=99=BA=E5=AD=90=20Kevin=20Deng?= Date: Thu, 2 May 2024 22:37:49 +0900 Subject: [PATCH 14/16] refactor: merge --- packages/compiler-vapor/src/generators/slotOutlet.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/compiler-vapor/src/generators/slotOutlet.ts b/packages/compiler-vapor/src/generators/slotOutlet.ts index 713de0206..0e3c5371c 100644 --- a/packages/compiler-vapor/src/generators/slotOutlet.ts +++ b/packages/compiler-vapor/src/generators/slotOutlet.ts @@ -19,8 +19,9 @@ export function genSlotOutlet(oper: SlotOutletIRNode, context: CodegenContext) { fallbackArg = genBlock(fallback, context) } - push(NEWLINE, `const n${id} = `) push( + NEWLINE, + `const n${id} = `, ...genCall( vaporHelper('createSlot'), nameExpr, From 71800da7ed17cc4f233366863f88746d1b6a456f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=89=E5=92=B2=E6=99=BA=E5=AD=90=20Kevin=20Deng?= Date: Thu, 2 May 2024 23:14:19 +0900 Subject: [PATCH 15/16] fix: custom directive --- .../src/transforms/transformSlotOutlet.ts | 60 ++++++++++--------- 1 file changed, 32 insertions(+), 28 deletions(-) diff --git a/packages/compiler-vapor/src/transforms/transformSlotOutlet.ts b/packages/compiler-vapor/src/transforms/transformSlotOutlet.ts index cda48582e..71f2b2c5d 100644 --- a/packages/compiler-vapor/src/transforms/transformSlotOutlet.ts +++ b/packages/compiler-vapor/src/transforms/transformSlotOutlet.ts @@ -28,7 +28,6 @@ export const transformSlotOutlet: NodeTransform = (node, context) => { if (node.type !== NodeTypes.ELEMENT || node.tag !== 'slot') { return } - const { props } = node const id = context.reference() context.dynamic.flags |= DynamicFlag.INSERT const [fallback, exitBlock] = createFallback( @@ -36,60 +35,65 @@ export const transformSlotOutlet: NodeTransform = (node, context) => { context as TransformContext, ) - let name: SimpleExpressionNode | undefined - const nonNameProps: (AttributeNode | DirectiveNode)[] = [] - for (const prop of props) { + 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') { - name = createSimpleExpression(prop.value.content, true, prop.loc) + slotName = createSimpleExpression(prop.value.content, true, prop.loc) } else { - nonNameProps.push(extend({}, prop, { name: camelize(prop.name) })) + slotProps.push(extend({}, prop, { name: camelize(prop.name) })) } } } else if (prop.name === 'bind' && isStaticArgOf(prop.arg, 'name')) { if (prop.exp) { - name = (prop as VaporDirectiveNode).exp! - } else if (prop.arg && prop.arg.type === NodeTypes.SIMPLE_EXPRESSION) { + slotName = prop.exp! + } else { // v-bind shorthand syntax - name = createSimpleExpression( - camelize(prop.arg.content), + slotName = createSimpleExpression( + camelize(prop.arg!.content), false, - prop.arg.loc, + prop.arg!.loc, ) - name.ast = null + slotName.ast = null } } else { - const nonProp = extend({}, prop) - if (nonProp.name === 'bind' && nonProp.arg && isStaticExp(nonProp.arg)) { - nonProp.arg = extend({}, nonProp.arg, { - content: camelize(nonProp.arg.content), + let slotProp = prop + if ( + slotProp.name === 'bind' && + slotProp.arg && + isStaticExp(slotProp.arg) + ) { + slotProp = extend({}, prop, { + arg: extend({}, slotProp.arg, { + content: camelize(slotProp.arg!.content), + }), }) } - nonNameProps.push(nonProp) + slotProps.push(slotProp) } } - name ||= createSimpleExpression('default', true) + slotName ||= createSimpleExpression('default', true) let irProps: IRProps[] = [] - if (nonNameProps.length > 0) { + if (slotProps.length) { const [isDynamic, props] = buildProps( - extend({}, node, { props: nonNameProps }), + extend({}, node, { props: slotProps }), context as TransformContext, true, ) irProps = isDynamic ? props : [props] - const { operation } = context.block - const hasDirectives = operation.filter( - oper => oper.type === IRNodeTypes.WITH_DIRECTIVE, - ) as WithDirectiveIRNode[] - - if (hasDirectives.length) { + 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, - hasDirectives[0].dir.loc, + runtimeDirective.dir.loc, ), ) } @@ -100,7 +104,7 @@ export const transformSlotOutlet: NodeTransform = (node, context) => { context.registerOperation({ type: IRNodeTypes.SLOT_OUTLET_NODE, id, - name, + name: slotName, props: irProps, fallback, }) From fd25071ab7d4db567b1b862a86ce0c304fc9e734 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=89=E5=92=B2=E6=99=BA=E5=AD=90=20Kevin=20Deng?= Date: Thu, 2 May 2024 23:24:50 +0900 Subject: [PATCH 16/16] refactor --- packages/compiler-vapor/src/transforms/transformElement.ts | 5 +++-- .../compiler-vapor/src/transforms/transformSlotOutlet.ts | 7 +++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/packages/compiler-vapor/src/transforms/transformElement.ts b/packages/compiler-vapor/src/transforms/transformElement.ts index d0e75784d..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,7 +180,7 @@ function transformNativeElement( export type PropsResult = | [dynamic: true, props: IRProps[], expressions: SimpleExpressionNode[]] - | [dynamic: false, props: IRProp[]] + | [dynamic: false, props: IRPropsStatic] export function buildProps( node: ElementNode, diff --git a/packages/compiler-vapor/src/transforms/transformSlotOutlet.ts b/packages/compiler-vapor/src/transforms/transformSlotOutlet.ts index 71f2b2c5d..5db2e1fcb 100644 --- a/packages/compiler-vapor/src/transforms/transformSlotOutlet.ts +++ b/packages/compiler-vapor/src/transforms/transformSlotOutlet.ts @@ -1,6 +1,5 @@ import { type AttributeNode, - type DirectiveNode, type ElementNode, ElementTypes, ErrorCodes, @@ -29,7 +28,7 @@ export const transformSlotOutlet: NodeTransform = (node, context) => { return } const id = context.reference() - context.dynamic.flags |= DynamicFlag.INSERT + context.dynamic.flags |= DynamicFlag.INSERT | DynamicFlag.NON_TEMPLATE const [fallback, exitBlock] = createFallback( node, context as TransformContext, @@ -124,10 +123,10 @@ function createFallback( tag: 'template', props: [], tagType: ElementTypes.TEMPLATE, - children: [...node.children], + children: node.children, }) - const fallback: BlockIRNode = newBlock(node) + const fallback = newBlock(node) const exitBlock = context.enterBlock(fallback) context.reference() return [fallback, exitBlock]