This commit is contained in:
zhiyuanzmj 2025-06-27 06:36:30 +00:00 committed by GitHub
commit 7914078ba4
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
34 changed files with 309 additions and 122 deletions

View File

@ -27,7 +27,8 @@ const slotProp = ref('slot prop')
change slot prop change slot prop
</button> </button>
<div class="vdom-slot-in-vapor-default"> <div class="vdom-slot-in-vapor-default">
#default: <slot :foo="slotProp" /> #default:
<slot :foo="slotProp" />
</div> </div>
<div class="vdom-slot-in-vapor-test"> <div class="vdom-slot-in-vapor-test">
#test: <slot name="test">fallback content</slot> #test: <slot name="test">fallback content</slot>
@ -40,7 +41,7 @@ const slotProp = ref('slot prop')
> >
Toggle default slot to vdom Toggle default slot to vdom
</button> </button>
<VdomComp :msg="msg"> <VdomComp :msg="msg" class="foo">
<template #default="{ foo }" v-if="passSlot"> <template #default="{ foo }" v-if="passSlot">
<div>slot prop: {{ foo }}</div> <div>slot prop: {{ foo }}</div>
<div>component prop: {{ msg }}</div> <div>component prop: {{ msg }}</div>

View File

@ -449,7 +449,7 @@ describe('compiler: transform v-model', () => {
expect(codegen.dynamicProps).toBe(`["modelValue", "onUpdate:modelValue"]`) expect(codegen.dynamicProps).toBe(`["modelValue", "onUpdate:modelValue"]`)
}) })
test('should generate modelModifiers for component v-model', () => { test('should generate modelValueModifiers for component v-model', () => {
const root = parseWithVModel('<Comp v-model.trim.bar-baz="foo" />', { const root = parseWithVModel('<Comp v-model.trim.bar-baz="foo" />', {
prefixIdentifiers: true, prefixIdentifiers: true,
}) })
@ -461,7 +461,7 @@ describe('compiler: transform v-model', () => {
{ key: { content: `modelValue` } }, { key: { content: `modelValue` } },
{ key: { content: `onUpdate:modelValue` } }, { key: { content: `onUpdate:modelValue` } },
{ {
key: { content: 'modelModifiers' }, key: { content: 'modelValueModifiers' },
value: { value: {
content: `{ trim: true, "bar-baz": true }`, content: `{ trim: true, "bar-baz": true }`,
isStatic: false, isStatic: false,
@ -469,7 +469,7 @@ describe('compiler: transform v-model', () => {
}, },
], ],
}) })
// should NOT include modelModifiers in dynamicPropNames because it's never // should NOT include modelValueModifiers in dynamicPropNames because it's never
// gonna change // gonna change
expect(vnodeCall.dynamicProps).toBe(`["modelValue", "onUpdate:modelValue"]`) expect(vnodeCall.dynamicProps).toBe(`["modelValue", "onUpdate:modelValue"]`)
}) })

View File

@ -30,10 +30,6 @@ export function walkIdentifiers(
parentStack: Node[] = [], parentStack: Node[] = [],
knownIds: Record<string, number> = Object.create(null), knownIds: Record<string, number> = Object.create(null),
): void { ): void {
if (__BROWSER__) {
return
}
const rootExp = const rootExp =
root.type === 'Program' root.type === 'Program'
? root.body[0].type === 'ExpressionStatement' && root.body[0].expression ? root.body[0].type === 'ExpressionStatement' && root.body[0].expression
@ -110,10 +106,6 @@ export function isReferencedIdentifier(
parent: Node | null, parent: Node | null,
parentStack: Node[], parentStack: Node[],
): boolean { ): boolean {
if (__BROWSER__) {
return false
}
if (!parent) { if (!parent) {
return true return true
} }

View File

@ -138,7 +138,7 @@ export const transformModel: DirectiveTransform = (dir, node, context) => {
? isStaticExp(arg) ? isStaticExp(arg)
? `${arg.content}Modifiers` ? `${arg.content}Modifiers`
: createCompoundExpression([arg, ' + "Modifiers"']) : createCompoundExpression([arg, ' + "Modifiers"'])
: `modelModifiers` : `modelValueModifiers`
props.push( props.push(
createObjectProperty( createObjectProperty(
modifiersKey, modifiersKey,

View File

@ -6,7 +6,7 @@ exports[`defineModel() > basic usage 1`] = `
export default { export default {
props: { props: {
"modelValue": { required: true }, "modelValue": { required: true },
"modelModifiers": {}, "modelValueModifiers": {},
"count": {}, "count": {},
"countModifiers": {}, "countModifiers": {},
"toString": { type: Function }, "toString": { type: Function },
@ -34,7 +34,7 @@ export default /*@__PURE__*/_defineComponent({
"modelValue": { "modelValue": {
required: true required: true
}, },
"modelModifiers": {}, "modelValueModifiers": {},
}, },
emits: ["update:modelValue"], emits: ["update:modelValue"],
setup(__props, { expose: __expose }) { setup(__props, { expose: __expose }) {
@ -60,7 +60,7 @@ export default /*@__PURE__*/_defineComponent({
default: 0, default: 0,
required: true, required: true,
}, },
"modelModifiers": {}, "modelValueModifiers": {},
}, },
emits: ["update:modelValue"], emits: ["update:modelValue"],
setup(__props, { expose: __expose }) { setup(__props, { expose: __expose }) {
@ -86,7 +86,7 @@ export default /*@__PURE__*/_defineComponent({
}, { }, {
"modelValue": { "modelValue": {
}, },
"modelModifiers": {}, "modelValueModifiers": {},
}), }),
emits: ["update:modelValue"], emits: ["update:modelValue"],
setup(__props: any, { expose: __expose }) { setup(__props: any, { expose: __expose }) {
@ -109,7 +109,7 @@ exports[`defineModel() > w/ Boolean And Function types, production mode 1`] = `
export default /*@__PURE__*/_defineComponent({ export default /*@__PURE__*/_defineComponent({
props: { props: {
"modelValue": { type: [Boolean, String] }, "modelValue": { type: [Boolean, String] },
"modelModifiers": {}, "modelValueModifiers": {},
}, },
emits: ["update:modelValue"], emits: ["update:modelValue"],
setup(__props, { expose: __expose }) { setup(__props, { expose: __expose }) {
@ -150,7 +150,7 @@ exports[`defineModel() > w/ defineProps and defineEmits 1`] = `
export default { export default {
props: /*@__PURE__*/_mergeModels({ foo: String }, { props: /*@__PURE__*/_mergeModels({ foo: String }, {
"modelValue": { default: 0 }, "modelValue": { default: 0 },
"modelModifiers": {}, "modelValueModifiers": {},
}), }),
emits: /*@__PURE__*/_mergeModels(['change'], ["update:modelValue"]), emits: /*@__PURE__*/_mergeModels(['change'], ["update:modelValue"]),
setup(__props, { expose: __expose }) { setup(__props, { expose: __expose }) {
@ -172,7 +172,7 @@ exports[`defineModel() > w/ types, basic usage 1`] = `
export default /*@__PURE__*/_defineComponent({ export default /*@__PURE__*/_defineComponent({
props: { props: {
"modelValue": { type: [Boolean, String] }, "modelValue": { type: [Boolean, String] },
"modelModifiers": {}, "modelValueModifiers": {},
"count": { type: Number }, "count": { type: Number },
"countModifiers": {}, "countModifiers": {},
"disabled": { type: Number, ...{ required: false } }, "disabled": { type: Number, ...{ required: false } },
@ -201,7 +201,7 @@ exports[`defineModel() > w/ types, production mode 1`] = `
export default /*@__PURE__*/_defineComponent({ export default /*@__PURE__*/_defineComponent({
props: { props: {
"modelValue": { type: Boolean }, "modelValue": { type: Boolean },
"modelModifiers": {}, "modelValueModifiers": {},
"fn": {}, "fn": {},
"fnModifiers": {}, "fnModifiers": {},
"fnWithDefault": { type: Function, ...{ default: () => null } }, "fnWithDefault": { type: Function, ...{ default: () => null } },
@ -233,7 +233,7 @@ exports[`defineModel() > w/ types, production mode, boolean + multiple types 1`]
export default /*@__PURE__*/_defineComponent({ export default /*@__PURE__*/_defineComponent({
props: { props: {
"modelValue": { type: [Boolean, String, Object] }, "modelValue": { type: [Boolean, String, Object] },
"modelModifiers": {}, "modelValueModifiers": {},
}, },
emits: ["update:modelValue"], emits: ["update:modelValue"],
setup(__props, { expose: __expose }) { setup(__props, { expose: __expose }) {
@ -253,7 +253,7 @@ exports[`defineModel() > w/ types, production mode, function + runtime opts + mu
export default /*@__PURE__*/_defineComponent({ export default /*@__PURE__*/_defineComponent({
props: { props: {
"modelValue": { type: [Number, Function], ...{ default: () => 1 } }, "modelValue": { type: [Number, Function], ...{ default: () => 1 } },
"modelModifiers": {}, "modelValueModifiers": {},
}, },
emits: ["update:modelValue"], emits: ["update:modelValue"],
setup(__props, { expose: __expose }) { setup(__props, { expose: __expose }) {

View File

@ -94,7 +94,7 @@ describe('defineModel()', () => {
) )
assertCode(content) assertCode(content)
expect(content).toMatch('"modelValue": { type: [Boolean, String] }') expect(content).toMatch('"modelValue": { type: [Boolean, String] }')
expect(content).toMatch('"modelModifiers": {}') expect(content).toMatch('"modelValueModifiers": {}')
expect(content).toMatch('"count": { type: Number }') expect(content).toMatch('"count": { type: Number }')
expect(content).toMatch( expect(content).toMatch(
'"disabled": { type: Number, ...{ required: false } }', '"disabled": { type: Number, ...{ required: false } }',

View File

@ -167,9 +167,7 @@ export function genModelProps(ctx: ScriptCompileContext) {
modelPropsDecl += `\n ${JSON.stringify(name)}: ${decl},` modelPropsDecl += `\n ${JSON.stringify(name)}: ${decl},`
// also generate modifiers prop // also generate modifiers prop
const modifierPropName = JSON.stringify( const modifierPropName = JSON.stringify(`${name}Modifiers`)
name === 'modelValue' ? `modelModifiers` : `${name}Modifiers`,
)
modelPropsDecl += `\n ${modifierPropName}: {},` modelPropsDecl += `\n ${modifierPropName}: {},`
} }
return `{${modelPropsDecl}\n }` return `{${modelPropsDecl}\n }`

View File

@ -246,6 +246,36 @@ export function render(_ctx) {
}" }"
`; `;
exports[`compiler: element transform > component event with keys modifier 1`] = `
"import { resolveComponent as _resolveComponent, withKeys as _withKeys, createComponentWithFallback as _createComponentWithFallback } from 'vue';
export function render(_ctx) {
const _component_Foo = _resolveComponent("Foo")
const n0 = _createComponentWithFallback(_component_Foo, { onKeyup: () => _withKeys(_ctx.bar, ["enter"]) }, null, true)
return n0
}"
`;
exports[`compiler: element transform > component event with multiple modifiers and event options 1`] = `
"import { resolveComponent as _resolveComponent, withModifiers as _withModifiers, withKeys as _withKeys, createComponentWithFallback as _createComponentWithFallback } from 'vue';
export function render(_ctx) {
const _component_Foo = _resolveComponent("Foo")
const n0 = _createComponentWithFallback(_component_Foo, { onFooCaptureOnce: () => _withKeys(_withModifiers(_ctx.bar, ["stop","prevent"]), ["enter"]) }, null, true)
return n0
}"
`;
exports[`compiler: element transform > component event with nonKeys modifier 1`] = `
"import { resolveComponent as _resolveComponent, withModifiers as _withModifiers, createComponentWithFallback as _createComponentWithFallback } from 'vue';
export function render(_ctx) {
const _component_Foo = _resolveComponent("Foo")
const n0 = _createComponentWithFallback(_component_Foo, { onFoo: () => _withModifiers(_ctx.bar, ["stop","prevent"]) }, null, true)
return n0
}"
`;
exports[`compiler: element transform > component event with once modifier 1`] = ` exports[`compiler: element transform > component event with once modifier 1`] = `
"import { resolveComponent as _resolveComponent, createComponentWithFallback as _createComponentWithFallback } from 'vue'; "import { resolveComponent as _resolveComponent, createComponentWithFallback as _createComponentWithFallback } from 'vue';

View File

@ -1,13 +1,13 @@
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
exports[`compiler: vModel transform > component > v-model for component should generate modelModifiers 1`] = ` exports[`compiler: vModel transform > component > v-model for component should generate modelValueModifiers 1`] = `
"import { resolveComponent as _resolveComponent, createComponentWithFallback as _createComponentWithFallback } from 'vue'; "import { resolveComponent as _resolveComponent, createComponentWithFallback as _createComponentWithFallback } from 'vue';
export function render(_ctx) { export function render(_ctx) {
const _component_Comp = _resolveComponent("Comp") const _component_Comp = _resolveComponent("Comp")
const n0 = _createComponentWithFallback(_component_Comp, { modelValue: () => (_ctx.foo), const n0 = _createComponentWithFallback(_component_Comp, { modelValue: () => (_ctx.foo),
"onUpdate:modelValue": () => _value => (_ctx.foo = _value), "onUpdate:modelValue": () => _value => (_ctx.foo = _value),
modelModifiers: () => ({ trim: true, "bar-baz": true }) }, null, true) modelValueModifiers: () => ({ trim: true, "bar-baz": true }) }, null, true)
return n0 return n0
}" }"
`; `;

View File

@ -896,6 +896,78 @@ describe('compiler: element transform', () => {
}) })
}) })
test('component event with keys modifier', () => {
const { code, ir } = compileWithElementTransform(
`<Foo @keyup.enter="bar" />`,
)
expect(code).toMatchSnapshot()
expect(ir.block.dynamic.children[0].operation).toMatchObject({
type: IRNodeTypes.CREATE_COMPONENT_NODE,
tag: 'Foo',
props: [
[
{
key: { content: 'keyup' },
handler: true,
handlerModifiers: {
keys: ['enter'],
nonKeys: [],
options: [],
},
},
],
],
})
})
test('component event with nonKeys modifier', () => {
const { code, ir } = compileWithElementTransform(
`<Foo @foo.stop.prevent="bar" />`,
)
expect(code).toMatchSnapshot()
expect(ir.block.dynamic.children[0].operation).toMatchObject({
type: IRNodeTypes.CREATE_COMPONENT_NODE,
tag: 'Foo',
props: [
[
{
key: { content: 'foo' },
handler: true,
handlerModifiers: {
keys: [],
nonKeys: ['stop', 'prevent'],
options: [],
},
},
],
],
})
})
test('component event with multiple modifiers and event options', () => {
const { code, ir } = compileWithElementTransform(
`<Foo @foo.enter.stop.prevent.capture.once="bar" />`,
)
expect(code).toMatchSnapshot()
expect(ir.block.dynamic.children[0].operation).toMatchObject({
type: IRNodeTypes.CREATE_COMPONENT_NODE,
tag: 'Foo',
props: [
[
{
key: { content: 'foo' },
handler: true,
handlerModifiers: {
keys: ['enter'],
nonKeys: ['stop', 'prevent'],
options: ['capture', 'once'],
},
},
],
],
})
})
test('component with dynamic event arguments', () => { test('component with dynamic event arguments', () => {
const { code, ir } = compileWithElementTransform( const { code, ir } = compileWithElementTransform(
`<Foo @[foo-bar]="bar" @[baz]="qux" />`, `<Foo @[foo-bar]="bar" @[baz]="qux" />`,

View File

@ -266,13 +266,13 @@ describe('compiler: vModel transform', () => {
}) })
}) })
test('v-model for component should generate modelModifiers', () => { test('v-model for component should generate modelValueModifiers', () => {
const { code, ir } = compileWithVModel( const { code, ir } = compileWithVModel(
'<Comp v-model.trim.bar-baz="foo" />', '<Comp v-model.trim.bar-baz="foo" />',
) )
expect(code).toMatchSnapshot() expect(code).toMatchSnapshot()
expect(code).contain( expect(code).contain(
`modelModifiers: () => ({ trim: true, "bar-baz": true })`, `modelValueModifiers: () => ({ trim: true, "bar-baz": true })`,
) )
expect(ir.block.dynamic.children[0].operation).toMatchObject({ expect(ir.block.dynamic.children[0].operation).toMatchObject({
type: IRNodeTypes.CREATE_COMPONENT_NODE, type: IRNodeTypes.CREATE_COMPONENT_NODE,

View File

@ -19,7 +19,14 @@ import {
} from './generators/utils' } from './generators/utils'
import { setTemplateRefIdent } from './generators/templateRef' import { setTemplateRefIdent } from './generators/templateRef'
export type CodegenOptions = Omit<BaseCodegenOptions, 'optimizeImports'> type CustomGenOperation = (
opers: any,
context: CodegenContext,
) => CodeFragment[] | void
export type CodegenOptions = Omit<BaseCodegenOptions, 'optimizeImports'> & {
customGenOperation?: CustomGenOperation | null
}
export class CodegenContext { export class CodegenContext {
options: Required<CodegenOptions> options: Required<CodegenOptions>
@ -87,6 +94,7 @@ export class CodegenContext {
inline: false, inline: false,
bindingMetadata: {}, bindingMetadata: {},
expressionPlugins: [], expressionPlugins: [],
customGenOperation: null,
} }
this.options = extend(defaultOptions, options) this.options = extend(defaultOptions, options)
this.block = ir.block this.block = ir.block

View File

@ -211,7 +211,7 @@ function genProp(prop: IRProp, context: CodegenContext, isStatic?: boolean) {
? genEventHandler( ? genEventHandler(
context, context,
prop.values[0], prop.values[0],
undefined, prop.handlerModifiers,
true /* wrap handlers passed to components */, true /* wrap handlers passed to components */,
) )
: isStatic : isStatic
@ -240,9 +240,7 @@ function genModelModifiers(
if (!modelModifiers || !modelModifiers.length) return [] if (!modelModifiers || !modelModifiers.length) return []
const modifiersKey = key.isStatic const modifiersKey = key.isStatic
? key.content === 'modelValue' ? [`${key.content}Modifiers`]
? [`modelModifiers`]
: [`${key.content}Modifiers`]
: ['[', ...genExpression(key, context), ' + "Modifiers"]'] : ['[', ...genExpression(key, context), ' + "Modifiers"]']
const modifiersVal = genDirectiveModifiers(modelModifiers) const modifiersVal = genDirectiveModifiers(modelModifiers)

View File

@ -88,6 +88,11 @@ export function genOperation(
case IRNodeTypes.GET_TEXT_CHILD: case IRNodeTypes.GET_TEXT_CHILD:
return genGetTextChild(oper, context) return genGetTextChild(oper, context)
default: default:
if (context.options.customGenOperation) {
const result = context.options.customGenOperation(oper, context)
if (result) return result
}
const exhaustiveCheck: never = oper const exhaustiveCheck: never = oper
throw new Error( throw new Error(
`Unhandled operation type in genOperation: ${exhaustiveCheck}`, `Unhandled operation type in genOperation: ${exhaustiveCheck}`,

View File

@ -114,9 +114,10 @@ export function genPropKey(
): CodeFragment[] { ): CodeFragment[] {
const { helper } = context const { helper } = context
const handlerModifierPostfix = handlerModifiers const handlerModifierPostfix =
? handlerModifiers.map(capitalize).join('') handlerModifiers && handlerModifiers.options
: '' ? handlerModifiers.options.map(capitalize).join('')
: ''
// static arg was transformed by v-bind transformer // static arg was transformed by v-bind transformer
if (node.isStatic) { if (node.isStatic) {
// only quote keys if necessary // only quote keys if necessary

View File

@ -10,8 +10,8 @@ export function genSetText(
context: CodegenContext, context: CodegenContext,
): CodeFragment[] { ): CodeFragment[] {
const { helper } = context const { helper } = context
const { element, values, generated, jsx } = oper const { element, values, generated } = oper
const texts = combineValues(values, context, jsx) const texts = combineValues(values, context)
return [ return [
NEWLINE, NEWLINE,
...genCall(helper('setText'), `${generated ? 'x' : 'n'}${element}`, texts), ...genCall(helper('setText'), `${generated ? 'x' : 'n'}${element}`, texts),
@ -21,16 +21,15 @@ export function genSetText(
function combineValues( function combineValues(
values: SimpleExpressionNode[], values: SimpleExpressionNode[],
context: CodegenContext, context: CodegenContext,
jsx?: boolean,
): CodeFragment[] { ): CodeFragment[] {
return values.flatMap((value, i) => { return values.flatMap((value, i) => {
let exp = genExpression(value, context) let exp = genExpression(value, context)
if (!jsx && getLiteralExpressionValue(value) == null) { if (getLiteralExpressionValue(value) == null) {
// dynamic, wrap with toDisplayString // dynamic, wrap with toDisplayString
exp = genCall(context.helper('toDisplayString'), exp) exp = genCall(context.helper('toDisplayString'), exp)
} }
if (i > 0) { if (i > 0) {
exp.unshift(jsx ? ', ' : ' + ') exp.unshift(' + ')
} }
return exp return exp
}) })

View File

@ -10,6 +10,8 @@ import {
import { isArray, isString } from '@vue/shared' import { isArray, isString } from '@vue/shared'
import type { CodegenContext } from '../generate' import type { CodegenContext } from '../generate'
export { genExpression } from './expression'
export const NEWLINE: unique symbol = Symbol(__DEV__ ? `newline` : ``) export const NEWLINE: unique symbol = Symbol(__DEV__ ? `newline` : ``)
/** increase offset but don't push actual code */ /** increase offset but don't push actual code */
export const LF: unique symbol = Symbol(__DEV__ ? `line feed` : ``) export const LF: unique symbol = Symbol(__DEV__ ? `line feed` : ``)

View File

@ -13,13 +13,7 @@ export {
type CodegenOptions, type CodegenOptions,
type VaporCodegenResult, type VaporCodegenResult,
} from './generate' } from './generate'
export { export * from './generators/utils'
genCall,
genMulti,
buildCodeFragment,
codeFragmentToString,
type CodeFragment,
} from './generators/utils'
export { export {
wrapTemplate, wrapTemplate,
compile, compile,

View File

@ -123,7 +123,6 @@ export interface SetTextIRNode extends BaseIRNode {
element: number element: number
values: SimpleExpressionNode[] values: SimpleExpressionNode[]
generated?: boolean // whether this is a generated empty text node by `processTextLikeContainer` generated?: boolean // whether this is a generated empty text node by `processTextLikeContainer`
jsx?: boolean
} }
export type KeyOverride = [find: string, replacement: string] export type KeyOverride = [find: string, replacement: string]

View File

@ -24,6 +24,7 @@ import {
type IRSlots, type IRSlots,
type OperationNode, type OperationNode,
type RootIRNode, type RootIRNode,
type SetEventIRNode,
type VaporDirectiveNode, type VaporDirectiveNode,
} from './ir' } from './ir'
import { isConstantExpression, isStaticExpression } from './utils' import { isConstantExpression, isStaticExpression } from './utils'
@ -46,7 +47,7 @@ export interface DirectiveTransformResult {
modifier?: '.' | '^' modifier?: '.' | '^'
runtimeCamelize?: boolean runtimeCamelize?: boolean
handler?: boolean handler?: boolean
handlerModifiers?: string[] handlerModifiers?: SetEventIRNode['modifiers']
model?: boolean model?: boolean
modelModifiers?: string[] modelModifiers?: string[]
} }

View File

@ -65,7 +65,11 @@ export const transformVOn: DirectiveTransform = (dir, node, context) => {
key: arg, key: arg,
value: handler, value: handler,
handler: true, handler: true,
handlerModifiers: eventOptionModifiers, handlerModifiers: {
keys: keyModifiers,
nonKeys: nonKeyModifiers,
options: eventOptionModifiers,
},
} }
} }

View File

@ -325,7 +325,7 @@ describe('component: emit', () => {
const Comp = () => const Comp = () =>
h(Foo, { h(Foo, {
modelValue: null, modelValue: null,
modelModifiers: { number: true }, modelValueModifiers: { number: true },
'onUpdate:modelValue': fn1, 'onUpdate:modelValue': fn1,
foo: null, foo: null,
@ -356,7 +356,7 @@ describe('component: emit', () => {
const Comp = () => const Comp = () =>
h(Foo, { h(Foo, {
modelValue: null, modelValue: null,
modelModifiers: { trim: true }, modelValueModifiers: { trim: true },
'onUpdate:modelValue': fn1, 'onUpdate:modelValue': fn1,
foo: null, foo: null,
@ -410,7 +410,7 @@ describe('component: emit', () => {
const Comp = () => const Comp = () =>
h(Foo, { h(Foo, {
modelValue: null, modelValue: null,
modelModifiers: { trim: true }, modelValueModifiers: { trim: true },
'onUpdate:modelValue': fn1, 'onUpdate:modelValue': fn1,
firstName: null, firstName: null,
@ -464,7 +464,7 @@ describe('component: emit', () => {
const Comp = () => const Comp = () =>
h(Foo, { h(Foo, {
modelValue: null, modelValue: null,
modelModifiers: { trim: true, number: true }, modelValueModifiers: { trim: true, number: true },
'onUpdate:modelValue': fn1, 'onUpdate:modelValue': fn1,
foo: null, foo: null,
@ -492,7 +492,7 @@ describe('component: emit', () => {
const Comp = () => const Comp = () =>
h(Foo, { h(Foo, {
modelValue: null, modelValue: null,
modelModifiers: { trim: true }, modelValueModifiers: { trim: true },
'onUpdate:modelValue': fn, 'onUpdate:modelValue': fn,
}) })

View File

@ -35,6 +35,7 @@ import { ErrorCodes, callWithAsyncErrorHandling } from './errorHandling'
import type { DefineComponent } from './apiDefineComponent' import type { DefineComponent } from './apiDefineComponent'
export interface App<HostElement = any> { export interface App<HostElement = any> {
vapor?: boolean
version: string version: string
config: AppConfig config: AppConfig

View File

@ -16,10 +16,25 @@ export let currentInstance: GenericComponentInstance | null = null
export const getCurrentGenericInstance: () => GenericComponentInstance | null = export const getCurrentGenericInstance: () => GenericComponentInstance | null =
() => currentInstance || currentRenderingInstance () => currentInstance || currentRenderingInstance
export const getCurrentInstance: () => ComponentInternalInstance | null = () => /**
currentInstance && !currentInstance.vapor * Retrieves the current component instance.
? (currentInstance as ComponentInternalInstance) *
* @param generic - A boolean flag indicating whether to return a generic component instance.
* If `true`, returns a `GenericComponentInstance` including vapor instance.
* If `false` or unset, returns a `ComponentInternalInstance` if available.
* @returns The current component instance, or `null` if no instance is active.
*/
export function getCurrentInstance(
generic: true,
): GenericComponentInstance | null
export function getCurrentInstance(
generic?: boolean,
): ComponentInternalInstance | null
export function getCurrentInstance(generic?: boolean) {
return currentInstance && (generic || !currentInstance.vapor)
? currentInstance
: currentRenderingInstance : currentRenderingInstance
}
export let isInSSRComponentSetup = false export let isInSSRComponentSetup = false

View File

@ -145,9 +145,9 @@ export const getModelModifiers = (
modelName: string, modelName: string,
getter: (props: Record<string, any>, key: string) => any, getter: (props: Record<string, any>, key: string) => any,
): Record<string, boolean> | undefined => { ): Record<string, boolean> | undefined => {
return modelName === 'modelValue' || modelName === 'model-value' return (
? getter(props, 'modelModifiers') getter(props, `${modelName}Modifiers`) ||
: getter(props, `${modelName}Modifiers`) || getter(props, `${camelize(modelName)}Modifiers`) ||
getter(props, `${camelize(modelName)}Modifiers`) || getter(props, `${hyphenate(modelName)}Modifiers`)
getter(props, `${hyphenate(modelName)}Modifiers`) )
} }

View File

@ -331,25 +331,29 @@ describe('component', () => {
__DEV__ = true __DEV__ = true
}) })
it('warn if functional vapor component not return a block', () => { it('functional vapor component return a object', () => {
define(() => { const { host } = define(() => {
return () => {} return {}
}).render() }).render()
expect( expect(host.textContent).toBe(`[object Object]`)
'Functional vapor component must return a block directly',
).toHaveBeenWarned()
}) })
it('warn if setup return a function and no render function', () => { it('functional vapor component return a function', () => {
define({ const { host } = define(() => {
return () => ({})
}).render()
expect(host.textContent).toBe(`() => ({})`)
})
it('setup return a function and no render function', () => {
const { host } = define({
setup() { setup() {
return () => [] return () => []
}, },
}).render() }).render()
expect( expect(host.textContent).toBe(`() => []`)
'Vapor component setup() returned non-block value, and has no render function',
).toHaveBeenWarned()
}) })
}) })

View File

@ -265,7 +265,7 @@ describe('component: emit', () => {
const fn2 = vi.fn() const fn2 = vi.fn()
render({ render({
modelValue: () => null, modelValue: () => null,
modelModifiers: () => ({ number: true }), modelValueModifiers: () => ({ number: true }),
['onUpdate:modelValue']: () => fn1, ['onUpdate:modelValue']: () => fn1,
foo: () => null, foo: () => null,
fooModifiers: () => ({ number: true }), fooModifiers: () => ({ number: true }),
@ -291,7 +291,7 @@ describe('component: emit', () => {
modelValue() { modelValue() {
return null return null
}, },
modelModifiers() { modelValueModifiers() {
return { trim: true } return { trim: true }
}, },
['onUpdate:modelValue']() { ['onUpdate:modelValue']() {
@ -327,7 +327,7 @@ describe('component: emit', () => {
modelValue() { modelValue() {
return null return null
}, },
modelModifiers() { modelValueModifiers() {
return { trim: true, number: true } return { trim: true, number: true }
}, },
['onUpdate:modelValue']() { ['onUpdate:modelValue']() {
@ -361,7 +361,7 @@ describe('component: emit', () => {
modelValue() { modelValue() {
return null return null
}, },
modelModifiers() { modelValueModifiers() {
return { trim: true } return { trim: true }
}, },
['onUpdate:modelValue']() { ['onUpdate:modelValue']() {

View File

@ -0,0 +1,25 @@
import { createTextNode, normalizeNode } from '../../src/dom/node'
import { VaporFragment } from '../../src'
describe('dom node', () => {
test('normalizeNode', () => {
// null / undefined -> Comment
expect(normalizeNode(null)).toBeInstanceOf(Comment)
expect(normalizeNode(undefined)).toBeInstanceOf(Comment)
// boolean -> Comment
expect(normalizeNode(true)).toBeInstanceOf(Comment)
expect(normalizeNode(false)).toBeInstanceOf(Comment)
// array -> Fragment
expect(normalizeNode(['foo'])).toBeInstanceOf(VaporFragment)
// VNode -> VNode
const vnode = createTextNode('div')
expect(normalizeNode(vnode)).toBe(vnode)
// primitive types
expect(normalizeNode('foo')).toMatchObject(createTextNode('foo'))
expect(normalizeNode(1)).toMatchObject(createTextNode('1'))
})
})

View File

@ -182,7 +182,6 @@ describe('error handling', () => {
define(Comp).render() define(Comp).render()
expect(fn).toHaveBeenCalledWith(err, 'setup function') expect(fn).toHaveBeenCalledWith(err, 'setup function')
expect(`returned non-block value`).toHaveBeenWarned()
}) })
test('in render function', () => { test('in render function', () => {

View File

@ -100,6 +100,7 @@ function postPrepareApp(app: App) {
) )
} }
app.vapor = true
const mount = app.mount const mount = app.mount
app.mount = (container, ...args: any[]) => { app.mount = (container, ...args: any[]) => {
container = normalizeContainer(container) as ParentNode container = normalizeContainer(container) as ParentNode

View File

@ -23,9 +23,8 @@ import {
simpleSetCurrentInstance, simpleSetCurrentInstance,
startMeasure, startMeasure,
unregisterHMR, unregisterHMR,
warn,
} from '@vue/runtime-dom' } from '@vue/runtime-dom'
import { type Block, DynamicFragment, insert, isBlock, remove } from './block' import { type Block, DynamicFragment, insert, remove } from './block'
import { import {
type ShallowRef, type ShallowRef,
markRaw, markRaw,
@ -59,6 +58,7 @@ import {
} from './componentSlots' } from './componentSlots'
import { hmrReload, hmrRerender } from './hmr' import { hmrReload, hmrRerender } from './hmr'
import { isHydrating, locateHydrationNode } from './dom/hydration' import { isHydrating, locateHydrationNode } from './dom/hydration'
import { normalizeNode } from './dom/node'
import { import {
insertionAnchor, insertionAnchor,
insertionParent, insertionParent,
@ -150,8 +150,9 @@ export function createComponent(
resetInsertionState() resetInsertionState()
} }
const isFnComponent = isFunction(component)
// vdom interop enabled and component is not an explicit vapor component // vdom interop enabled and component is not an explicit vapor component
if (appContext.vapor && !component.__vapor) { if (appContext.vapor && !isFnComponent && !component.__vapor) {
const frag = appContext.vapor.vdomMount( const frag = appContext.vapor.vdomMount(
component as any, component as any,
rawProps, rawProps,
@ -188,6 +189,14 @@ export function createComponent(
appContext, appContext,
) )
// HMR
if (__DEV__ && component.__hmrId) {
registerHMR(instance)
instance.isSingleRoot = isSingleRoot
instance.hmrRerender = hmrRerender.bind(null, instance)
instance.hmrReload = hmrReload.bind(null, instance)
}
if (__DEV__) { if (__DEV__) {
pushWarningContext(instance) pushWarningContext(instance)
startMeasure(instance, `init`) startMeasure(instance, `init`)
@ -205,36 +214,22 @@ export function createComponent(
setupPropsValidation(instance) setupPropsValidation(instance)
} }
const setupFn = isFunction(component) ? component : component.setup const setupFn = isFnComponent ? component : component.setup
const setupResult = setupFn const setupResult = setupFn
? callWithErrorHandling(setupFn, instance, ErrorCodes.SETUP_FUNCTION, [ ? callWithErrorHandling(setupFn, instance, ErrorCodes.SETUP_FUNCTION, [
instance.props, instance.props,
instance, instance,
]) || EMPTY_OBJ ]) || []
: EMPTY_OBJ : []
if (__DEV__ && !isBlock(setupResult)) { if (__DEV__) {
if (isFunction(component)) { if (isFnComponent || !component.render) {
warn(`Functional vapor component must return a block directly.`) instance.block = normalizeNode(setupResult)
instance.block = []
} else if (!component.render) {
warn(
`Vapor component setup() returned non-block value, and has no render function.`,
)
instance.block = []
} else { } else {
instance.devtoolsRawSetupState = setupResult instance.devtoolsRawSetupState = setupResult
// TODO make the proxy warn non-existent property access during dev // TODO make the proxy warn non-existent property access during dev
instance.setupState = proxyRefs(setupResult) instance.setupState = proxyRefs(setupResult)
devRender(instance) devRender(instance)
// HMR
if (component.__hmrId) {
registerHMR(instance)
instance.isSingleRoot = isSingleRoot
instance.hmrRerender = hmrRerender.bind(null, instance)
instance.hmrReload = hmrReload.bind(null, instance)
}
} }
} else { } else {
// component has a render function but no setup function // component has a render function but no setup function
@ -247,7 +242,7 @@ export function createComponent(
) )
} else { } else {
// in prod result can only be block // in prod result can only be block
instance.block = setupResult as Block instance.block = normalizeNode(setupResult)
} }
} }
@ -291,18 +286,33 @@ export let isApplyingFallthroughProps = false
*/ */
export function devRender(instance: VaporComponentInstance): void { export function devRender(instance: VaporComponentInstance): void {
instance.block = instance.block =
callWithErrorHandling( (instance.type.render
instance.type.render!, ? callWithErrorHandling(
instance, instance.type.render,
ErrorCodes.RENDER_FUNCTION, instance,
[ ErrorCodes.RENDER_FUNCTION,
instance.setupState, [
instance.props, instance.setupState,
instance.emit, instance.props,
instance.attrs, instance.emit,
instance.slots, instance.attrs,
], instance.slots,
) || [] ],
)
: callWithErrorHandling(
isFunction(instance.type) ? instance.type : instance.type.setup!,
instance,
ErrorCodes.SETUP_FUNCTION,
[
instance.props,
{
slots: instance.slots,
attrs: instance.attrs,
emit: instance.emit,
expose: instance.expose,
},
],
)) || []
} }
const emptyContext: GenericAppContext = { const emptyContext: GenericAppContext = {

View File

@ -1,3 +1,6 @@
import { type Block, VaporFragment, isBlock } from '../block'
import { isArray } from '@vue/shared'
/*! #__NO_SIDE_EFFECTS__ */ /*! #__NO_SIDE_EFFECTS__ */
export function createTextNode(value = ''): Text { export function createTextNode(value = ''): Text {
return document.createTextNode(value) return document.createTextNode(value)
@ -27,3 +30,24 @@ export function nthChild(node: Node, i: number): Node {
export function next(node: Node): Node { export function next(node: Node): Node {
return node.nextSibling! return node.nextSibling!
} }
type NodeChildAtom = Node | string | number | boolean | null | undefined | void
export type NodeArrayChildren = Array<NodeArrayChildren | NodeChildAtom>
export type NodeChild = NodeChildAtom | NodeArrayChildren
export function normalizeNode(node: NodeChild): Block {
if (node == null || typeof node === 'boolean') {
// empty placeholder
return createComment('')
} else if (isArray(node) && node.length) {
// fragment
return new VaporFragment(node.map(normalizeNode))
} else if (isBlock(node)) {
return node
} else {
// strings and numbers
return createTextNode(String(node))
}
}

View File

@ -7,7 +7,11 @@ export type { VaporDirective } from './directives/custom'
// compiler-use only // compiler-use only
export { insert, prepend, remove, isFragment, VaporFragment } from './block' export { insert, prepend, remove, isFragment, VaporFragment } from './block'
export { setInsertionState } from './insertionState' export { setInsertionState } from './insertionState'
export { createComponent, createComponentWithFallback } from './component' export {
createComponent,
createComponentWithFallback,
isVaporComponent,
} from './component'
export { renderEffect } from './renderEffect' export { renderEffect } from './renderEffect'
export { createSlot } from './componentSlots' export { createSlot } from './componentSlots'
export { template } from './dom/template' export { template } from './dom/template'

View File

@ -154,7 +154,7 @@ function createVDOMComponent(
const frag = new VaporFragment([]) const frag = new VaporFragment([])
const vnode = createVNode( const vnode = createVNode(
component, component,
rawProps && new Proxy(rawProps, rawPropsProxyHandlers), rawProps && extend({}, new Proxy(rawProps, rawPropsProxyHandlers)),
) )
const wrapper = new VaporComponentInstance( const wrapper = new VaporComponentInstance(
{ props: component.props }, { props: component.props },