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

View File

@ -449,7 +449,7 @@ describe('compiler: transform v-model', () => {
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" />', {
prefixIdentifiers: true,
})
@ -461,7 +461,7 @@ describe('compiler: transform v-model', () => {
{ key: { content: `modelValue` } },
{ key: { content: `onUpdate:modelValue` } },
{
key: { content: 'modelModifiers' },
key: { content: 'modelValueModifiers' },
value: {
content: `{ trim: true, "bar-baz": true }`,
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
expect(vnodeCall.dynamicProps).toBe(`["modelValue", "onUpdate:modelValue"]`)
})

View File

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

View File

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

View File

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

View File

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

View File

@ -167,9 +167,7 @@ export function genModelProps(ctx: ScriptCompileContext) {
modelPropsDecl += `\n ${JSON.stringify(name)}: ${decl},`
// also generate modifiers prop
const modifierPropName = JSON.stringify(
name === 'modelValue' ? `modelModifiers` : `${name}Modifiers`,
)
const modifierPropName = JSON.stringify(`${name}Modifiers`)
modelPropsDecl += `\n ${modifierPropName}: {},`
}
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`] = `
"import { resolveComponent as _resolveComponent, createComponentWithFallback as _createComponentWithFallback } from 'vue';

View File

@ -1,13 +1,13 @@
// 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';
export function render(_ctx) {
const _component_Comp = _resolveComponent("Comp")
const n0 = _createComponentWithFallback(_component_Comp, { modelValue: () => (_ctx.foo),
"onUpdate:modelValue": () => _value => (_ctx.foo = _value),
modelModifiers: () => ({ trim: true, "bar-baz": true }) }, null, true)
modelValueModifiers: () => ({ trim: true, "bar-baz": true }) }, null, true)
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', () => {
const { code, ir } = compileWithElementTransform(
`<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(
'<Comp v-model.trim.bar-baz="foo" />',
)
expect(code).toMatchSnapshot()
expect(code).contain(
`modelModifiers: () => ({ trim: true, "bar-baz": true })`,
`modelValueModifiers: () => ({ trim: true, "bar-baz": true })`,
)
expect(ir.block.dynamic.children[0].operation).toMatchObject({
type: IRNodeTypes.CREATE_COMPONENT_NODE,

View File

@ -19,7 +19,14 @@ import {
} from './generators/utils'
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 {
options: Required<CodegenOptions>
@ -87,6 +94,7 @@ export class CodegenContext {
inline: false,
bindingMetadata: {},
expressionPlugins: [],
customGenOperation: null,
}
this.options = extend(defaultOptions, options)
this.block = ir.block

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -16,10 +16,25 @@ export let currentInstance: GenericComponentInstance | null = null
export const getCurrentGenericInstance: () => GenericComponentInstance | null =
() => currentInstance || currentRenderingInstance
export const getCurrentInstance: () => ComponentInternalInstance | null = () =>
currentInstance && !currentInstance.vapor
? (currentInstance as ComponentInternalInstance)
/**
* Retrieves the current component instance.
*
* @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
}
export let isInSSRComponentSetup = false

View File

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

View File

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

View File

@ -265,7 +265,7 @@ describe('component: emit', () => {
const fn2 = vi.fn()
render({
modelValue: () => null,
modelModifiers: () => ({ number: true }),
modelValueModifiers: () => ({ number: true }),
['onUpdate:modelValue']: () => fn1,
foo: () => null,
fooModifiers: () => ({ number: true }),
@ -291,7 +291,7 @@ describe('component: emit', () => {
modelValue() {
return null
},
modelModifiers() {
modelValueModifiers() {
return { trim: true }
},
['onUpdate:modelValue']() {
@ -327,7 +327,7 @@ describe('component: emit', () => {
modelValue() {
return null
},
modelModifiers() {
modelValueModifiers() {
return { trim: true, number: true }
},
['onUpdate:modelValue']() {
@ -361,7 +361,7 @@ describe('component: emit', () => {
modelValue() {
return null
},
modelModifiers() {
modelValueModifiers() {
return { trim: true }
},
['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()
expect(fn).toHaveBeenCalledWith(err, 'setup function')
expect(`returned non-block value`).toHaveBeenWarned()
})
test('in render function', () => {

View File

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

View File

@ -23,9 +23,8 @@ import {
simpleSetCurrentInstance,
startMeasure,
unregisterHMR,
warn,
} from '@vue/runtime-dom'
import { type Block, DynamicFragment, insert, isBlock, remove } from './block'
import { type Block, DynamicFragment, insert, remove } from './block'
import {
type ShallowRef,
markRaw,
@ -59,6 +58,7 @@ import {
} from './componentSlots'
import { hmrReload, hmrRerender } from './hmr'
import { isHydrating, locateHydrationNode } from './dom/hydration'
import { normalizeNode } from './dom/node'
import {
insertionAnchor,
insertionParent,
@ -150,8 +150,9 @@ export function createComponent(
resetInsertionState()
}
const isFnComponent = isFunction(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(
component as any,
rawProps,
@ -188,6 +189,14 @@ export function createComponent(
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__) {
pushWarningContext(instance)
startMeasure(instance, `init`)
@ -205,36 +214,22 @@ export function createComponent(
setupPropsValidation(instance)
}
const setupFn = isFunction(component) ? component : component.setup
const setupFn = isFnComponent ? component : component.setup
const setupResult = setupFn
? callWithErrorHandling(setupFn, instance, ErrorCodes.SETUP_FUNCTION, [
instance.props,
instance,
]) || EMPTY_OBJ
: EMPTY_OBJ
]) || []
: []
if (__DEV__ && !isBlock(setupResult)) {
if (isFunction(component)) {
warn(`Functional vapor component must return a block directly.`)
instance.block = []
} else if (!component.render) {
warn(
`Vapor component setup() returned non-block value, and has no render function.`,
)
instance.block = []
if (__DEV__) {
if (isFnComponent || !component.render) {
instance.block = normalizeNode(setupResult)
} else {
instance.devtoolsRawSetupState = setupResult
// TODO make the proxy warn non-existent property access during dev
instance.setupState = proxyRefs(setupResult)
devRender(instance)
// HMR
if (component.__hmrId) {
registerHMR(instance)
instance.isSingleRoot = isSingleRoot
instance.hmrRerender = hmrRerender.bind(null, instance)
instance.hmrReload = hmrReload.bind(null, instance)
}
}
} else {
// component has a render function but no setup function
@ -247,7 +242,7 @@ export function createComponent(
)
} else {
// 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 {
instance.block =
callWithErrorHandling(
instance.type.render!,
instance,
ErrorCodes.RENDER_FUNCTION,
[
instance.setupState,
instance.props,
instance.emit,
instance.attrs,
instance.slots,
],
) || []
(instance.type.render
? callWithErrorHandling(
instance.type.render,
instance,
ErrorCodes.RENDER_FUNCTION,
[
instance.setupState,
instance.props,
instance.emit,
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 = {

View File

@ -1,3 +1,6 @@
import { type Block, VaporFragment, isBlock } from '../block'
import { isArray } from '@vue/shared'
/*! #__NO_SIDE_EFFECTS__ */
export function createTextNode(value = ''): Text {
return document.createTextNode(value)
@ -27,3 +30,24 @@ export function nthChild(node: Node, i: number): Node {
export function next(node: Node): Node {
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
export { insert, prepend, remove, isFragment, VaporFragment } from './block'
export { setInsertionState } from './insertionState'
export { createComponent, createComponentWithFallback } from './component'
export {
createComponent,
createComponentWithFallback,
isVaporComponent,
} from './component'
export { renderEffect } from './renderEffect'
export { createSlot } from './componentSlots'
export { template } from './dom/template'

View File

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