feat: create component & component lifecycle/props/attrs (#151)

This commit is contained in:
Kevin Deng 三咲智子 2024-03-16 18:54:36 +08:00 committed by GitHub
parent 5d15314c4e
commit 463b47e83d
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
31 changed files with 922 additions and 611 deletions

View File

@ -120,18 +120,19 @@ export function render(_ctx) {
`;
exports[`compile > directives > v-pre > should not affect siblings after it 1`] = `
"import { createTextNode as _createTextNode, insert as _insert, renderEffect as _renderEffect, setText as _setText, setDynamicProp as _setDynamicProp, template as _template } from 'vue/vapor';
"import { resolveComponent as _resolveComponent, createComponent as _createComponent, createTextNode as _createTextNode, insert as _insert, renderEffect as _renderEffect, setText as _setText, setDynamicProp as _setDynamicProp, template as _template } from 'vue/vapor';
const t0 = _template("<div :id=\\"foo\\"><Comp></Comp>{{ bar }}</div>")
const t1 = _template("<div><Comp></Comp></div>")
const t1 = _template("<div></div>")
export function render(_ctx) {
const n0 = t0()
const n2 = t1()
const n1 = _createTextNode()
_insert(n1, n2)
_renderEffect(() => _setText(n1, _ctx.bar))
_renderEffect(() => _setDynamicProp(n2, "id", _ctx.foo))
return [n0, n2]
const n3 = t1()
const n1 = _createComponent(_resolveComponent("Comp"))
const n2 = _createTextNode()
_insert([n1, n2], n3)
_renderEffect(() => _setText(n2, _ctx.bar))
_renderEffect(() => _setDynamicProp(n3, "id", _ctx.foo))
return [n0, n3]
}"
`;

View File

@ -72,7 +72,6 @@ describe('compile', () => {
expect(code).not.contains('effect')
})
// TODO: support multiple root nodes and components
test('should not affect siblings after it', () => {
const code = compile(
`<div v-pre :id="foo"><Comp/>{{ bar }}</div>\n` +

View File

@ -0,0 +1,64 @@
import { isArray } from '@vue/shared'
import type { CodegenContext } from '../generate'
import type { CreateComponentIRNode, IRProp } from '../ir'
import {
type CodeFragment,
INDENT_END,
INDENT_START,
NEWLINE,
genCall,
genMulti,
} from './utils'
import { genExpression } from './expression'
import { genPropKey } from './prop'
export function genCreateComponent(
oper: CreateComponentIRNode,
context: CodegenContext,
): CodeFragment[] {
const { vaporHelper } = context
const tag = oper.resolve
? genCall(vaporHelper('resolveComponent'), JSON.stringify(oper.tag))
: [oper.tag]
return [
NEWLINE,
`const n${oper.id} = `,
...genCall(vaporHelper('createComponent'), tag, genProps()),
]
function genProps() {
const props = oper.props
.map(props => {
if (isArray(props)) {
if (!props.length) return undefined
return genStaticProps(props)
} else {
return ['() => (', ...genExpression(props, context), ')']
}
})
.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),
': () => (',
...genExpression(prop.values[0], context),
')',
]
}),
)
}
}

View File

@ -16,6 +16,7 @@ import {
NEWLINE,
buildCodeFragment,
} from './utils'
import { genCreateComponent } from './component'
export function genOperations(opers: OperationNode[], context: CodegenContext) {
const [frag, push] = buildCodeFragment()
@ -56,6 +57,8 @@ export function genOperation(
return genIf(oper, context)
case IRNodeTypes.FOR:
return genFor(oper, context)
case IRNodeTypes.CREATE_COMPONENT_NODE:
return genCreateComponent(oper, context)
}
return []

View File

@ -78,14 +78,14 @@ function genLiteralObjectProps(
return genMulti(
['{ ', ' }', ', '],
...props.map(prop => [
...genPropertyKey(prop, context),
...genPropKey(prop, context),
`: `,
...genPropValue(prop.values, context),
]),
)
}
function genPropertyKey(
export function genPropKey(
{ key: node, runtimeCamelize, modifier }: IRProp,
context: CodegenContext,
): CodeFragment[] {

View File

@ -29,6 +29,7 @@ export enum IRNodeTypes {
INSERT_NODE,
PREPEND_NODE,
CREATE_TEXT_NODE,
CREATE_COMPONENT_NODE,
WITH_DIRECTIVE,
@ -173,6 +174,16 @@ export interface WithDirectiveIRNode extends BaseIRNode {
builtin?: VaporHelper
}
export interface CreateComponentIRNode extends BaseIRNode {
type: IRNodeTypes.CREATE_COMPONENT_NODE
id: number
tag: string
props: IRProps[]
// TODO slots
resolve: boolean
}
export type IRNode = OperationNode | RootIRNode
export type OperationNode =
| SetPropIRNode
@ -189,6 +200,7 @@ export type OperationNode =
| WithDirectiveIRNode
| IfIRNode
| ForIRNode
| CreateComponentIRNode
export enum DynamicFlag {
NONE = 0,

View File

@ -15,6 +15,7 @@ import type {
TransformContext,
} from '../transform'
import {
DynamicFlag,
IRNodeTypes,
type IRProp,
type IRProps,
@ -29,8 +30,7 @@ export const isReservedProp = /*#__PURE__*/ makeMap(
export const transformElement: NodeTransform = (node, context) => {
return function postTransformElement() {
node = context.node
;({ node } = context)
if (
!(
node.type === NodeTypes.ELEMENT &&
@ -41,37 +41,94 @@ export const transformElement: NodeTransform = (node, context) => {
return
}
const { tag, props } = node
const isComponent = node.tagType === ElementTypes.COMPONENT
context.template += `<${tag}`
if (props.length) {
buildProps(
const { tag, tagType } = node
const isComponent = tagType === ElementTypes.COMPONENT
const propsResult = buildProps(
node,
context as TransformContext<ElementNode>,
undefined,
isComponent,
)
;(isComponent ? transformComponentElement : transformNativeElement)(
tag,
propsResult,
context,
)
}
const { scopeId } = context.options
if (scopeId) {
context.template += ` ${scopeId}`
}
context.template += `>` + context.childrenTemplate.join('')
}
function transformComponentElement(
tag: string,
propsResult: PropsResult,
context: TransformContext,
) {
const { bindingMetadata } = context.options
const resolve = !bindingMetadata[tag]
context.dynamic.flags |= DynamicFlag.NON_TEMPLATE | DynamicFlag.INSERT
context.registerOperation({
type: IRNodeTypes.CREATE_COMPONENT_NODE,
id: context.reference(),
tag,
props: propsResult[0] ? propsResult[1] : [propsResult[1]],
resolve,
})
}
function transformNativeElement(
tag: string,
propsResult: ReturnType<typeof buildProps>,
context: TransformContext,
) {
const { scopeId } = context.options
context.template += `<${tag}`
if (scopeId) context.template += ` ${scopeId}`
if (propsResult[0] /* dynamic props */) {
const [, dynamicArgs, expressions] = propsResult
context.registerEffect(expressions, [
{
type: IRNodeTypes.SET_DYNAMIC_PROPS,
element: context.reference(),
props: dynamicArgs,
},
])
} else {
for (const prop of propsResult[1]) {
const { key, values } = prop
if (key.isStatic && values.length === 1 && values[0].isStatic) {
context.template += ` ${key.content}`
if (values[0].content) context.template += `="${values[0].content}"`
} else {
context.registerEffect(values, [
{
type: IRNodeTypes.SET_PROP,
element: context.reference(),
prop,
},
])
}
}
}
context.template += `>` + context.childrenTemplate.join('')
// TODO remove unnecessary close tag, e.g. if it's the last element of the template
if (!isVoidTag(tag)) {
context.template += `</${tag}>`
}
}
}
export type PropsResult =
| [dynamic: true, props: IRProps[], expressions: SimpleExpressionNode[]]
| [dynamic: false, props: IRProp[]]
function buildProps(
node: ElementNode,
context: TransformContext<ElementNode>,
props: (VaporDirectiveNode | AttributeNode)[] = node.props as any,
isComponent: boolean,
) {
): PropsResult {
const props = node.props as (VaporDirectiveNode | AttributeNode)[]
if (props.length === 0) return [false, []]
const dynamicArgs: IRProps[] = []
const dynamicExpr: SimpleExpressionNode[] = []
let results: DirectiveTransformResult[] = []
@ -112,31 +169,11 @@ function buildProps(
if (dynamicArgs.length || results.some(({ key }) => !key.isStatic)) {
// take rest of props as dynamic props
pushMergeArg()
context.registerEffect(dynamicExpr, [
{
type: IRNodeTypes.SET_DYNAMIC_PROPS,
element: context.reference(),
props: dynamicArgs,
},
])
} else {
return [true, dynamicArgs, dynamicExpr]
}
const irProps = dedupeProperties(results)
for (const prop of irProps) {
const { key, values } = prop
if (key.isStatic && values.length === 1 && values[0].isStatic) {
context.template += ` ${key.content}`
if (values[0].content) context.template += `="${values[0].content}"`
} else {
context.registerEffect(values, [
{
type: IRNodeTypes.SET_PROP,
element: context.reference(),
prop,
},
])
}
}
}
return [false, irProps]
}
function transformProp(

View File

@ -1,12 +1,12 @@
import type { Data } from '@vue/shared'
import {
type App,
type ComponentInternalInstance,
type ObjectComponent,
type SetupFn,
render as _render,
createComponentInstance,
createVaporApp,
defineComponent,
} from '../src'
import type { RawProps } from '../src/componentProps'
export function makeRender<Component = ObjectComponent | SetupFn>(
initHost = () => {
@ -27,18 +27,20 @@ export function makeRender<Component = ObjectComponent | SetupFn>(
const define = (comp: Component) => {
const component = defineComponent(comp as any)
let instance: ComponentInternalInstance
let app: App
const render = (
props: Data = {},
props: RawProps = {},
container: string | ParentNode = '#host',
) => {
instance = createComponentInstance(component, props)
_render(instance, container)
app = createVaporApp(component, props)
instance = app.mount(container)
return res()
}
const res = () => ({
component,
host,
instance,
app,
render,
})

View File

@ -1,4 +1,4 @@
import { ref, setText, template, unmountComponent, watchEffect } from '../src'
import { ref, setText, template, watchEffect } from '../src'
import { describe, expect } from 'vitest'
import { makeRender } from './_utils'
@ -6,7 +6,7 @@ const define = makeRender()
describe('component', () => {
test('unmountComponent', async () => {
const { host, instance } = define(() => {
const { host, app } = define(() => {
const count = ref(0)
const t0 = template('<div></div>')
const n0 = t0()
@ -16,7 +16,7 @@ describe('component', () => {
return n0
}).render()
expect(host.innerHTML).toBe('<div>0</div>')
unmountComponent(instance)
app.unmount()
expect(host.innerHTML).toBe('')
})
})

View File

@ -3,13 +3,13 @@
// Note: emits and listener fallthrough is tested in
// ./rendererAttrsFallthrough.spec.ts.
import { nextTick, onBeforeUnmount, unmountComponent } from '../src'
import { nextTick, onBeforeUnmount } from '../src'
import { isEmitListener } from '../src/componentEmits'
import { makeRender } from './_utils'
const define = makeRender<any>()
describe('component: emit', () => {
describe.todo('component: emit', () => {
test('trigger handlers', () => {
const { render } = define({
render() {},
@ -137,9 +137,7 @@ describe('component: emit', () => {
const fn2 = vi.fn()
render({
get onFoo() {
return [fn1, fn2]
},
onFoo: () => [fn1, fn2],
})
expect(fn1).toHaveBeenCalledTimes(1)
expect(fn1).toHaveBeenCalledWith(1)
@ -246,22 +244,22 @@ describe('component: emit', () => {
const fn1 = vi.fn()
const fn2 = vi.fn()
render({
get modelValue() {
modelValue() {
return null
},
get modelModifiers() {
modelModifiers() {
return { number: true }
},
get ['onUpdate:modelValue']() {
['onUpdate:modelValue']() {
return fn1
},
get foo() {
foo() {
return null
},
get fooModifiers() {
fooModifiers() {
return { number: true }
},
get ['onUpdate:foo']() {
['onUpdate:foo']() {
return fn2
},
})
@ -282,22 +280,22 @@ describe('component: emit', () => {
const fn1 = vi.fn()
const fn2 = vi.fn()
render({
get modelValue() {
modelValue() {
return null
},
get modelModifiers() {
modelModifiers() {
return { trim: true }
},
get ['onUpdate:modelValue']() {
['onUpdate:modelValue']() {
return fn1
},
get foo() {
foo() {
return null
},
get fooModifiers() {
fooModifiers() {
return { trim: true }
},
get 'onUpdate:foo'() {
'onUpdate:foo'() {
return fn2
},
})
@ -318,22 +316,22 @@ describe('component: emit', () => {
const fn1 = vi.fn()
const fn2 = vi.fn()
render({
get modelValue() {
modelValue() {
return null
},
get modelModifiers() {
modelModifiers() {
return { trim: true, number: true }
},
get ['onUpdate:modelValue']() {
['onUpdate:modelValue']() {
return fn1
},
get foo() {
foo() {
return null
},
get fooModifiers() {
fooModifiers() {
return { trim: true, number: true }
},
get ['onUpdate:foo']() {
['onUpdate:foo']() {
return fn2
},
})
@ -352,13 +350,13 @@ describe('component: emit', () => {
})
const fn = vi.fn()
render({
get modelValue() {
modelValue() {
return null
},
get modelModifiers() {
modelModifiers() {
return { trim: true }
},
get ['onUpdate:modelValue']() {
['onUpdate:modelValue']() {
return fn
},
})
@ -397,7 +395,7 @@ describe('component: emit', () => {
test('does not emit after unmount', async () => {
const fn = vi.fn()
const { instance } = define({
const { app } = define({
emits: ['closing'],
setup(_: any, { emit }: any) {
onBeforeUnmount(async () => {
@ -412,7 +410,7 @@ describe('component: emit', () => {
},
})
await nextTick()
unmountComponent(instance)
app.unmount()
await nextTick()
expect(fn).not.toHaveBeenCalled()
})

View File

@ -1,24 +1,24 @@
// NOTE: This test is implemented based on the case of `runtime-core/__test__/componentProps.spec.ts`.
// NOTE: not supported
// mixins
// caching
import { setCurrentInstance } from '../src/component'
import {
createComponent,
defineComponent,
getCurrentInstance,
nextTick,
ref,
setText,
template,
toRefs,
watch,
watchEffect,
} from '../src'
import { makeRender } from './_utils'
const define = makeRender<any>()
describe('component props (vapor)', () => {
describe('component: props', () => {
// NOTE: no proxy
test('stateful', () => {
let props: any
let attrs: any
@ -32,65 +32,51 @@ describe('component props (vapor)', () => {
},
})
render({
get fooBar() {
return 1
},
get bar() {
return 2
},
})
expect(props.fooBar).toEqual(1)
expect(attrs.bar).toEqual(2)
render({ fooBar: () => 1, bar: () => 2 })
expect(props).toEqual({ fooBar: 1 })
expect(attrs).toEqual({ bar: 2 })
// test passing kebab-case and resolving to camelCase
render({
get ['foo-bar']() {
return 2
},
get bar() {
return 3
},
get baz() {
return 4
},
})
expect(props.fooBar).toEqual(2)
expect(attrs.bar).toEqual(3)
expect(attrs.baz).toEqual(4)
render({ 'foo-bar': () => 2, bar: () => 3, baz: () => 4 })
expect(props).toEqual({ fooBar: 2 })
expect(attrs).toEqual({ bar: 3, baz: 4 })
// test updating kebab-case should not delete it (#955)
render({
get ['foo-bar']() {
return 3
},
get bar() {
return 3
},
get baz() {
return 4
},
get barBaz() {
return 5
},
})
expect(props.fooBar).toEqual(3)
expect(props.barBaz).toEqual(5)
expect(attrs.bar).toEqual(3)
expect(attrs.baz).toEqual(4)
render({ 'foo-bar': () => 3, bar: () => 3, baz: () => 4, barBaz: () => 5 })
expect(props).toEqual({ fooBar: 3, barBaz: 5 })
expect(attrs).toEqual({ bar: 3, baz: 4 })
render({
get qux() {
return 5
},
})
expect(props.fooBar).toBeUndefined()
expect(props.barBaz).toBeUndefined()
expect(attrs.qux).toEqual(5)
// remove the props with camelCase key (#1412)
render({ qux: () => 5 })
expect(props).toEqual({})
expect(attrs).toEqual({ qux: 5 })
})
test.todo('stateful with setup', () => {
// TODO:
test.fails('stateful with setup', () => {
let props: any
let attrs: any
const { render } = define({
props: ['foo'],
setup(_props: any, { attrs: _attrs }: any) {
return () => {
props = _props
attrs = _attrs
}
},
})
render({ foo: () => 1, bar: () => 2 })
expect(props).toEqual({ foo: 1 })
expect(attrs).toEqual({ bar: 2 })
render({ foo: () => 2, bar: () => 3, baz: () => 4 })
expect(props).toEqual({ foo: 2 })
expect(attrs).toEqual({ bar: 3, baz: 4 })
render({ qux: () => 5 })
expect(props).toEqual({})
expect(attrs).toEqual({ qux: 5 })
})
test('functional with declaration', () => {
@ -105,42 +91,19 @@ describe('component props (vapor)', () => {
})
Comp.props = ['foo']
render({
get foo() {
return 1
},
get bar() {
return 2
},
})
expect(props.foo).toEqual(1)
expect(attrs.bar).toEqual(2)
render({ foo: () => 1, bar: () => 2 })
expect(props).toEqual({ foo: 1 })
expect(attrs).toEqual({ bar: 2 })
render({
get foo() {
return 2
},
get bar() {
return 3
},
get baz() {
return 4
},
})
expect(props.foo).toEqual(2)
expect(attrs.bar).toEqual(3)
expect(attrs.baz).toEqual(4)
render({ foo: () => 2, bar: () => 3, baz: () => 4 })
expect(props).toEqual({ foo: 2 })
expect(attrs).toEqual({ bar: 3, baz: 4 })
render({
get qux() {
return 5
},
})
expect(props.foo).toBeUndefined()
expect(attrs.qux).toEqual(5)
render({ qux: () => 5 })
expect(props).toEqual({})
expect(attrs).toEqual({ qux: 5 })
})
// FIXME:
test('functional without declaration', () => {
let props: any
let attrs: any
@ -152,21 +115,15 @@ describe('component props (vapor)', () => {
return {}
})
render({
get foo() {
return 1
},
})
expect(props.foo).toEqual(1)
expect(attrs.foo).toEqual(1)
render({ foo: () => 1 })
expect(props).toEqual({ foo: 1 })
expect(attrs).toEqual({ foo: 1 })
expect(props).toBe(attrs)
render({
get foo() {
return 2
},
})
expect(props.foo).toEqual(2)
expect(attrs.foo).toEqual(2)
render({ bar: () => 2 })
expect(props).toEqual({ bar: 2 })
expect(attrs).toEqual({ bar: 2 })
expect(props).toBe(attrs)
})
test('boolean casting', () => {
@ -186,15 +143,16 @@ describe('component props (vapor)', () => {
render({
// absent should cast to false
bar: '', // empty string should cast to true
baz: 'baz', // same string should cast to true
qux: 'ok', // other values should be left in-tact (but raise warning)
bar: () => '', // empty string should cast to true
baz: () => 'baz', // same string should cast to true
qux: () => 'ok', // other values should be left in-tact (but raise warning)
})
expect(props.foo).toBe(false)
expect(props.bar).toBe(true)
expect(props.baz).toBe(true)
expect(props.qux).toBe('ok')
// expect('type check failed for prop "qux"').toHaveBeenWarned()
})
test('default value', () => {
@ -221,82 +179,57 @@ describe('component props (vapor)', () => {
},
})
render({
get foo() {
return 2
},
})
render({ foo: () => 2 })
expect(props.foo).toBe(2)
// const prevBar = props.bar
props.bar
expect(props.bar).toEqual({ a: 1 })
expect(props.baz).toEqual(defaultBaz)
// expect(defaultFn).toHaveBeenCalledTimes(1) // failed: (caching is not supported)
expect(defaultFn).toHaveBeenCalledTimes(3)
expect(defaultFn).toHaveBeenCalledTimes(1)
expect(defaultBaz).toHaveBeenCalledTimes(0)
// #999: updates should not cause default factory of unchanged prop to be
// called again
render({
get foo() {
return 3
},
})
render({ foo: () => 3 })
expect(props.foo).toBe(3)
expect(props.bar).toEqual({ a: 1 })
// expect(props.bar).toBe(prevBar) // failed: (caching is not supported)
// expect(defaultFn).toHaveBeenCalledTimes(1) // failed: caching is not supported (called 3 times)
// expect(defaultFn).toHaveBeenCalledTimes(1) // failed: caching is not supported (called 2 times)
render({
get bar() {
return { b: 2 }
},
})
render({ bar: () => ({ b: 2 }) })
expect(props.foo).toBe(1)
expect(props.bar).toEqual({ b: 2 })
// expect(defaultFn).toHaveBeenCalledTimes(1) // failed: caching is not supported (called 3 times)
// expect(defaultFn).toHaveBeenCalledTimes(1) // failed: caching is not supported (called 2 times)
render({
get foo() {
return 3
},
get bar() {
return { b: 3 }
},
foo: () => 3,
bar: () => ({ b: 3 }),
})
expect(props.foo).toBe(3)
expect(props.bar).toEqual({ b: 3 })
// expect(defaultFn).toHaveBeenCalledTimes(1) // failed: caching is not supported (called 3 times)
// expect(defaultFn).toHaveBeenCalledTimes(1) // failed: caching is not supported (called 2 times)
render({
get bar() {
return { b: 4 }
},
})
render({ bar: () => ({ b: 4 }) })
expect(props.foo).toBe(1)
expect(props.bar).toEqual({ b: 4 })
// expect(defaultFn).toHaveBeenCalledTimes(1) // failed: caching is not supported (called 3 times)
// expect(defaultFn).toHaveBeenCalledTimes(1) // failed: caching is not supported (called 2 times)
})
test.todo('using inject in default value factory', () => {
// TODO: impl inject
})
// NOTE: maybe it's unnecessary
// https://github.com/vuejs/core-vapor/pull/99#discussion_r1472647377
test('optimized props updates', async () => {
const renderChild = define({
const t0 = template('<div>')
const { component: Child } = define({
props: ['foo'],
render() {
const instance = getCurrentInstance()!
const t0 = template('<div></div>')
const n0 = t0()
watchEffect(() => {
setText(n0, instance.props.foo)
})
watchEffect(() => setText(n0, instance.props.foo))
return n0
},
}).render
})
const foo = ref(1)
const id = ref('a')
@ -305,54 +238,36 @@ describe('component props (vapor)', () => {
return { foo, id }
},
render(_ctx: Record<string, any>) {
const t0 = template('<div>')
const n0 = t0()
renderChild(
{
get foo() {
return _ctx.foo
},
get id() {
return _ctx.id
},
},
n0 as HTMLDivElement,
)
return n0
return createComponent(Child, {
foo: () => _ctx.foo,
id: () => _ctx.id,
})
},
}).render()
const reset = setCurrentInstance(instance)
// expect(host.innerHTML).toBe('<div id="a">1</div>') // TODO: Fallthrough Attributes
expect(host.innerHTML).toBe('<div><div>1</div></div>')
expect(host.innerHTML).toBe('<div>1</div>')
foo.value++
await nextTick()
// expect(host.innerHTML).toBe('<div id="a">2</div>') // TODO: Fallthrough Attributes
expect(host.innerHTML).toBe('<div><div>2</div></div>')
expect(host.innerHTML).toBe('<div>2</div>')
// id.value = 'b'
// await nextTick()
id.value = 'b'
await nextTick()
// expect(host.innerHTML).toBe('<div id="b">2</div>') // TODO: Fallthrough Attributes
reset()
})
describe('validator', () => {
test('validator should be called with two arguments', () => {
let args: any
const mockFn = vi.fn((..._args: any[]) => {
args = _args
return true
})
const mockFn = vi.fn((...args: any[]) => true)
const props = {
get foo() {
return 1
},
get bar() {
return 2
},
foo: () => 1,
bar: () => 2,
}
const t0 = template('<div/>')
define({
props: {
foo: {
@ -364,19 +279,11 @@ describe('component props (vapor)', () => {
},
},
render() {
const t0 = template('<div/>')
const n0 = t0()
return n0
return t0()
},
}).render(props)
expect(mockFn).toHaveBeenCalled()
// NOTE: Vapor Component props defined by getter. So, `props` not Equal to `{ foo: 1, bar: 2 }`
// expect(mockFn).toHaveBeenCalledWith(1, { foo: 1, bar: 2 })
expect(args.length).toBe(2)
expect(args[0]).toBe(1)
expect(args[1].foo).toEqual(1)
expect(args[1].bar).toEqual(2)
expect(mockFn).toHaveBeenCalledWith(1, { foo: 1, bar: 2 })
})
// TODO: impl setter and warnner
@ -401,16 +308,16 @@ describe('component props (vapor)', () => {
return n0
},
}).render!({
get foo() {
foo() {
return 1
},
get bar() {
bar() {
return 2
},
})
expect(
`Set operation on key "bar" failed: target is readonly.`,
`Set operation on key "bar" failed: taris readonly.`,
).toHaveBeenWarnedLast()
expect(mockFn).toHaveBeenCalledWith(2)
},
@ -450,70 +357,71 @@ describe('component props (vapor)', () => {
return () => null
},
}).render({
get ['foo-bar']() {
return 'hello'
},
['foo-bar']: () => 'hello',
})
expect(`Missing required prop: "fooBar"`).not.toHaveBeenWarned()
})
test('props type support BigInt', () => {
const t0 = template('<div>')
const { host } = define({
props: {
foo: BigInt,
},
render() {
const instance = getCurrentInstance()!
const t0 = template('<div></div>')
const n0 = t0()
watchEffect(() => {
setText(n0, instance.props.foo)
})
watchEffect(() => setText(n0, instance.props.foo))
return n0
},
}).render({
get foo() {
return BigInt(BigInt(100000111)) + BigInt(2000000000) * BigInt(30000000)
},
foo: () =>
BigInt(BigInt(100000111)) + BigInt(2000000000) * BigInt(30000000),
})
expect(host.innerHTML).toBe('<div>60000000100000111</div>')
})
// #3288
// #3474
test.todo(
'declared prop key should be present even if not passed',
async () => {
// let initialKeys: string[] = []
// const changeSpy = vi.fn()
// const passFoo = ref(false)
// const Comp = {
// props: ['foo'],
// setup() {
// const instance = getCurrentInstance()!
// initialKeys = Object.keys(instance.props)
// watchEffect(changeSpy)
// return {}
// },
// render() {
// return {}
// },
// }
// const Parent = createIf(
// () => passFoo.value,
// () => {
// return render(Comp , { foo: 1 }, host) // TODO: createComponent fn
// },
// )
// // expect(changeSpy).toHaveBeenCalledTimes(1)
},
'should cache the value returned from the default factory to avoid unnecessary watcher trigger',
() => {},
)
// #3288
test('declared prop key should be present even if not passed', async () => {
let initialKeys: string[] = []
const changeSpy = vi.fn()
const passFoo = ref(false)
const Comp: any = {
render() {},
props: {
foo: String,
},
setup(props: any) {
initialKeys = Object.keys(props)
const { foo } = toRefs(props)
watch(foo, changeSpy)
},
}
define(() =>
createComponent(Comp, [() => (passFoo.value ? { foo: () => 'ok' } : {})]),
).render()
expect(initialKeys).toMatchObject(['foo'])
passFoo.value = true
await nextTick()
expect(changeSpy).toHaveBeenCalledTimes(1)
})
// #3371
test.todo(`avoid double-setting props when casting`, async () => {
// TODO: proide, slots
})
test('support null in required + multiple-type declarations', () => {
// NOTE: type check is not supported
test.todo('support null in required + multiple-type declarations', () => {
const { render } = define({
props: {
foo: { type: [Function, null], required: true },
@ -522,11 +430,11 @@ describe('component props (vapor)', () => {
})
expect(() => {
render({ foo: () => {} })
render({ foo: () => () => {} })
}).not.toThrow()
expect(() => {
render({ foo: null })
render({ foo: () => null })
}).not.toThrow()
})
@ -537,22 +445,17 @@ describe('component props (vapor)', () => {
const instance = getCurrentInstance()!
const t0 = template('<div></div>')
const n0 = t0()
watchEffect(() => {
watchEffect(() =>
setText(
n0,
JSON.stringify(instance.attrs) + Object.keys(instance.attrs),
),
)
})
return n0
},
})
let attrs: any = {
get foo() {
return undefined
},
}
const attrs: any = { foo: () => undefined }
render(attrs)
expect(host.innerHTML).toBe(
@ -567,8 +470,20 @@ describe('component props (vapor)', () => {
type: String,
},
}
define({ props, render() {} }).render({ msg: 'test' })
define({ props, render() {} }).render({ msg: () => 'test' })
expect(Object.keys(props.msg).length).toBe(1)
})
test('should warn against reserved prop names', () => {
const { render } = define({
props: {
$foo: String,
},
render() {},
})
render({ msg: () => 'test' })
expect(`Invalid prop name: "$foo"`).toHaveBeenWarned()
})
})

View File

@ -1,5 +1,5 @@
import { insert, normalizeBlock, prepend, remove } from '../../src/dom/element'
import { fragmentKey } from '../../src/render'
import { fragmentKey } from '../../src/apiRender'
const node1 = document.createTextNode('node1')
const node2 = document.createTextNode('node2')

View File

@ -0,0 +1,18 @@
import {
type Component,
createComponentInstance,
currentInstance,
} from './component'
import { setupComponent } from './apiRender'
import type { RawProps } from './componentProps'
export function createComponent(comp: Component, rawProps: RawProps = null) {
const current = currentInstance!
const instance = createComponentInstance(comp, rawProps)
setupComponent(instance)
// register sub-component with current component for lifecycle management
current.comps.add(instance)
return instance.block
}

View File

@ -2,7 +2,7 @@ import { type EffectScope, effectScope, isReactive } from '@vue/reactivity'
import { isArray, isObject, isString } from '@vue/shared'
import { createComment, createTextNode, insert, remove } from './dom/element'
import { renderEffect } from './renderWatch'
import { type Block, type Fragment, fragmentKey } from './render'
import { type Block, type Fragment, fragmentKey } from './apiRender'
import { warn } from './warning'
interface ForBlock extends Fragment {

View File

@ -1,5 +1,5 @@
import { renderWatch } from './renderWatch'
import { type Block, type Fragment, fragmentKey } from './render'
import { type Block, type Fragment, fragmentKey } from './apiRender'
import { type EffectScope, effectScope } from '@vue/reactivity'
import { createComment, createTextNode, insert, remove } from './dom/element'

View File

@ -0,0 +1,103 @@
import { isObject } from '@vue/shared'
import {
type Component,
type ComponentInternalInstance,
createComponentInstance,
} from './component'
import { warn } from './warning'
import { version } from '.'
import { render, setupComponent, unmountComponent } from './apiRender'
import type { RawProps } from './componentProps'
export function createVaporApp(
rootComponent: Component,
rootProps: RawProps | null = null,
): App {
if (rootProps != null && !isObject(rootProps)) {
__DEV__ && warn(`root props passed to app.mount() must be an object.`)
rootProps = null
}
const context = createAppContext()
let instance: ComponentInternalInstance
const app: App = {
version,
get config() {
return context.config
},
set config(v) {
if (__DEV__) {
warn(
`app.config cannot be replaced. Modify individual options instead.`,
)
}
},
mount(rootContainer): any {
if (!instance) {
instance = createComponentInstance(rootComponent, rootProps)
setupComponent(instance)
render(instance, rootContainer)
return instance
} else if (__DEV__) {
warn(
`App has already been mounted.\n` +
`If you want to remount the same app, move your app creation logic ` +
`into a factory function and create fresh app instances for each ` +
`mount - e.g. \`const createMyApp = () => createApp(App)\``,
)
}
},
unmount() {
if (instance) {
unmountComponent(instance)
} else if (__DEV__) {
warn(`Cannot unmount an app that is not mounted.`)
}
},
}
return app
}
function createAppContext(): AppContext {
return {
app: null as any,
config: {
errorHandler: undefined,
warnHandler: undefined,
},
}
}
export interface App {
version: string
config: AppConfig
mount(
rootContainer: ParentNode | string,
isHydrate?: boolean,
): ComponentInternalInstance
unmount(): void
}
export interface AppConfig {
errorHandler?: (
err: unknown,
instance: ComponentInternalInstance | null,
info: string,
) => void
warnHandler?: (
msg: string,
instance: ComponentInternalInstance | null,
trace: string,
) => void
}
export interface AppContext {
app: App // for devtools
config: AppConfig
}

View File

@ -1,13 +1,10 @@
import { proxyRefs } from '@vue/reactivity'
import { invokeArrayFns, isArray, isFunction, isObject } from '@vue/shared'
import {
type ComponentInternalInstance,
setCurrentInstance,
unsetCurrentInstance,
} from './component'
import { invokeDirectiveHook } from './directives'
import { isArray, isFunction, isObject } from '@vue/shared'
import { type ComponentInternalInstance, setCurrentInstance } from './component'
import { insert, querySelector, remove } from './dom/element'
import { flushPostFlushCbs, queuePostRenderEffect } from './scheduler'
import { proxyRefs } from '@vue/reactivity'
import { invokeLifecycle } from './componentLifecycle'
import { VaporLifecycleHooks } from './apiLifecycle'
export const fragmentKey = Symbol(__DEV__ ? `fragmentKey` : ``)
@ -18,28 +15,9 @@ export type Fragment = {
[fragmentKey]: true
}
export function render(
instance: ComponentInternalInstance,
container: string | ParentNode,
): void {
mountComponent(instance, (container = normalizeContainer(container)))
flushPostFlushCbs()
}
function normalizeContainer(container: string | ParentNode): ParentNode {
return typeof container === 'string'
? (querySelector(container) as ParentNode)
: container
}
function mountComponent(
instance: ComponentInternalInstance,
container: ParentNode,
) {
instance.container = container
export function setupComponent(instance: ComponentInternalInstance): void {
const reset = setCurrentInstance(instance)
const block = instance.scope.run(() => {
instance.scope.run(() => {
const { component, props, emit, attrs } = instance
const ctx = { expose: () => {}, emit, attrs }
@ -70,40 +48,52 @@ function mountComponent(
block = []
}
return (instance.block = block)
})!
const { bm, m } = instance
})
reset()
}
export function render(
instance: ComponentInternalInstance,
container: string | ParentNode,
): void {
mountComponent(instance, (container = normalizeContainer(container)))
flushPostFlushCbs()
}
function normalizeContainer(container: string | ParentNode): ParentNode {
return typeof container === 'string'
? (querySelector(container) as ParentNode)
: container
}
function mountComponent(
instance: ComponentInternalInstance,
container: ParentNode,
) {
instance.container = container
// hook: beforeMount
bm && invokeArrayFns(bm)
invokeDirectiveHook(instance, 'beforeMount')
invokeLifecycle(instance, VaporLifecycleHooks.BEFORE_MOUNT, 'beforeMount')
insert(block, instance.container)
insert(instance.block!, instance.container)
instance.isMounted = true
// hook: mounted
queuePostRenderEffect(() => {
invokeDirectiveHook(instance, 'mounted')
m && invokeArrayFns(m)
})
reset()
invokeLifecycle(instance, VaporLifecycleHooks.MOUNTED, 'mounted', true)
return instance
}
export function unmountComponent(instance: ComponentInternalInstance) {
const { container, block, scope, um, bum } = instance
const { container, block, scope } = instance
// hook: beforeUnmount
bum && invokeArrayFns(bum)
invokeDirectiveHook(instance, 'beforeUnmount')
invokeLifecycle(instance, VaporLifecycleHooks.BEFORE_UNMOUNT, 'beforeUnmount')
scope.stop()
block && remove(block, container)
instance.isMounted = false
instance.isUnmounted = true
// hook: unmounted
invokeDirectiveHook(instance, 'unmounted')
um && invokeArrayFns(um)
unsetCurrentInstance()
invokeLifecycle(instance, VaporLifecycleHooks.UNMOUNTED, 'unmounted', true)
queuePostRenderEffect(() => (instance.isUnmounted = true))
}

View File

@ -1,11 +1,12 @@
import { EffectScope } from '@vue/reactivity'
import { EMPTY_OBJ, isFunction } from '@vue/shared'
import type { Block } from './render'
import type { Block } from './apiRender'
import type { DirectiveBinding } from './directives'
import {
type ComponentPropsOptions,
type NormalizedPropsOptions,
type NormalizedRawProps,
type RawProps,
initProps,
normalizePropsOptions,
} from './componentProps'
@ -37,33 +38,29 @@ type LifecycleHook<TFn = Function> = TFn[] | null
export interface ComponentInternalInstance {
uid: number
container: ParentNode
vapor: true
block: Block | null
container: ParentNode
parent: ComponentInternalInstance | null
scope: EffectScope
component: FunctionalComponent | ObjectComponent
comps: Set<ComponentInternalInstance>
dirs: Map<Node, DirectiveBinding[]>
// TODO: ExtraProps: key, ref, ...
rawProps: { [key: string]: any }
// normalized options
rawProps: NormalizedRawProps
propsOptions: NormalizedPropsOptions
emitsOptions: ObjectEmitsOptions | null
parent: ComponentInternalInstance | null
// state
props: Data
attrs: Data
setupState: Data
props: Data
emit: EmitFn
emitted: Record<string, boolean> | null
attrs: Data
refs: Data
vapor: true
/** directives */
dirs: Map<Node, DirectiveBinding[]>
// lifecycle
isMounted: boolean
isUnmounted: boolean
@ -141,37 +138,37 @@ export const unsetCurrentInstance = () => {
}
let uid = 0
export const createComponentInstance = (
export function createComponentInstance(
component: ObjectComponent | FunctionalComponent,
rawProps: Data,
): ComponentInternalInstance => {
rawProps: RawProps | null,
): ComponentInternalInstance {
const instance: ComponentInternalInstance = {
uid: uid++,
block: null,
container: null!, // set on mountComponent
scope: new EffectScope(true /* detached */)!,
component,
rawProps,
vapor: true,
// TODO: registory of parent
block: null,
container: null!,
// TODO
parent: null,
scope: new EffectScope(true /* detached */)!,
component,
comps: new Set(),
dirs: new Map(),
// resolved props and emits options
rawProps: null!, // set later
propsOptions: normalizePropsOptions(component),
emitsOptions: normalizeEmitsOptions(component),
// emit
emit: null!, // to be set immediately
emitted: null,
// state
props: EMPTY_OBJ,
attrs: EMPTY_OBJ,
setupState: EMPTY_OBJ,
props: EMPTY_OBJ,
emit: null!,
emitted: null,
attrs: EMPTY_OBJ,
refs: EMPTY_OBJ,
vapor: true,
dirs: new Map(),
// lifecycle
isMounted: false,
@ -227,8 +224,6 @@ export const createComponentInstance = (
*/
// [VaporLifecycleHooks.SERVER_PREFETCH]: null,
}
// TODO init first
initProps(instance, rawProps, !isFunction(component))
instance.emit = emit.bind(null, instance)

View File

@ -0,0 +1,44 @@
import { camelize, isFunction } from '@vue/shared'
import type { ComponentInternalInstance } from './component'
import { isEmitListener } from './componentEmits'
export function patchAttrs(instance: ComponentInternalInstance) {
const attrs = instance.attrs
const options = instance.propsOptions[0]
const keys = new Set<string>()
if (instance.rawProps.length)
for (const props of Array.from(instance.rawProps).reverse()) {
if (isFunction(props)) {
const resolved = props()
for (const rawKey in resolved) {
registerAttr(rawKey, () => resolved[rawKey])
}
} else {
for (const rawKey in props) {
registerAttr(rawKey, props[rawKey])
}
}
}
for (const key in attrs) {
if (!keys.has(key)) {
delete attrs[key]
}
}
function registerAttr(key: string, getter: () => unknown) {
if (
(!options || !(camelize(key) in options)) &&
!isEmitListener(instance.emitsOptions, key)
) {
keys.add(key)
if (key in attrs) return
Object.defineProperty(attrs, key, {
get: getter,
enumerable: true,
configurable: true,
})
}
}
}

View File

@ -1,5 +1,8 @@
// NOTE: runtime-core/src/componentEmits.ts
// TODO WIP
// @ts-nocheck
import {
EMPTY_OBJ,
type UnionToIntersection,
@ -45,6 +48,8 @@ export function emit(
...rawArgs: any[]
) {
if (instance.isUnmounted) return
// TODO
// @ts-expect-error
const { rawProps } = instance
let args = rawArgs

View File

@ -0,0 +1,34 @@
import { invokeArrayFns } from '@vue/shared'
import type { VaporLifecycleHooks } from './apiLifecycle'
import { type ComponentInternalInstance, setCurrentInstance } from './component'
import { queuePostRenderEffect } from './scheduler'
import { type DirectiveHookName, invokeDirectiveHook } from './directives'
export function invokeLifecycle(
instance: ComponentInternalInstance,
lifecycle: VaporLifecycleHooks,
directive: DirectiveHookName,
post?: boolean,
) {
invokeArrayFns(post ? [invokeSub, invokeCurrent] : [invokeCurrent, invokeSub])
function invokeCurrent() {
const hooks = instance[lifecycle]
if (hooks) {
const fn = () => {
const reset = setCurrentInstance(instance)
invokeArrayFns(hooks)
reset()
}
post ? queuePostRenderEffect(fn) : fn()
}
invokeDirectiveHook(instance, directive)
}
function invokeSub() {
instance.comps.forEach(comp =>
invokeLifecycle(comp, lifecycle, directive, post),
)
}
}

View File

@ -1,5 +1,3 @@
// NOTE: runtime-core/src/componentProps.ts
import {
type Data,
EMPTY_ARR,
@ -11,14 +9,15 @@ import {
isArray,
isFunction,
} from '@vue/shared'
import { shallowReactive, shallowReadonly, toRaw } from '@vue/reactivity'
import { baseWatch, shallowReactive } from '@vue/reactivity'
import { warn } from './warning'
import {
type Component,
type ComponentInternalInstance,
setCurrentInstance,
} from './component'
import { isEmitListener } from './componentEmits'
import { patchAttrs } from './componentAttrs'
import { createVaporPreScheduler } from './scheduler'
export type ComponentPropsOptions<P = Data> =
| ComponentObjectPropsOptions<P>
@ -69,108 +68,129 @@ type NormalizedProp =
})
export type NormalizedProps = Record<string, NormalizedProp>
export type NormalizedPropsOptions = [NormalizedProps, string[]] | []
export type NormalizedPropsOptions =
| [props: NormalizedProps, needCastKeys: string[]]
| []
type StaticProps = Record<string, () => unknown>
type DynamicProps = () => Data
export type NormalizedRawProps = Array<StaticProps | DynamicProps>
export type RawProps = NormalizedRawProps | StaticProps | null
export function initProps(
instance: ComponentInternalInstance,
rawProps: Data | null,
rawProps: RawProps,
isStateful: boolean,
) {
const props: Data = {}
const attrs: Data = {}
const attrs = (instance.attrs = shallowReactive<Data>({}))
const [options, needCastKeys] = instance.propsOptions
let hasAttrsChanged = false
let rawCastValues: Data | undefined
if (rawProps) {
for (let key in rawProps) {
const valueGetter = () => rawProps[key]
let camelKey
if (options && hasOwn(options, (camelKey = camelize(key)))) {
if (!needCastKeys || !needCastKeys.includes(camelKey)) {
// NOTE: must getter
// props[camelKey] = value
Object.defineProperty(props, camelKey, {
get() {
return valueGetter()
},
enumerable: true,
})
if (!rawProps) rawProps = []
else if (!isArray(rawProps)) rawProps = [rawProps]
instance.rawProps = rawProps
const [options] = instance.propsOptions
const hasDynamicProps = rawProps.some(isFunction)
if (options) {
if (hasDynamicProps) {
for (const key in options) {
const getter = () =>
getDynamicPropValue(rawProps as NormalizedRawProps, key)
registerProp(instance, props, key, getter, true)
}
} else {
// NOTE: must getter
// ;(rawCastValues || (rawCastValues = {}))[camelKey] = value
rawCastValues || (rawCastValues = {})
Object.defineProperty(rawCastValues, camelKey, {
get() {
return valueGetter()
},
enumerable: true,
})
}
} else if (!isEmitListener(instance.emitsOptions, key)) {
// if (!(key in attrs) || value !== attrs[key]) {
if (!(key in attrs)) {
// NOTE: must getter
// attrs[key] = value
Object.defineProperty(attrs, key, {
get() {
return valueGetter()
},
enumerable: true,
})
hasAttrsChanged = true
}
}
}
}
if (needCastKeys) {
const rawCurrentProps = toRaw(props)
const castValues = rawCastValues || EMPTY_OBJ
for (let i = 0; i < needCastKeys.length; i++) {
const key = needCastKeys[i]
// NOTE: must getter
// props[key] = resolvePropValue(
// options!,
// rawCurrentProps,
// key,
// castValues[key],
// instance,
// !hasOwn(castValues, key),
// )
Object.defineProperty(props, key, {
get() {
return resolvePropValue(
options!,
rawCurrentProps,
key,
castValues[key],
for (const key in options) {
const rawKey = rawProps[0] && getRawKey(rawProps[0] as StaticProps, key)
if (rawKey) {
registerProp(
instance,
!hasOwn(castValues, key),
props,
key,
(rawProps[0] as StaticProps)[rawKey],
)
},
})
} else {
registerProp(instance, props, key, undefined, false, true)
}
}
}
}
// validation
if (__DEV__) {
validateProps(rawProps || {}, props, instance)
validateProps(rawProps, props, options || {})
}
if (hasDynamicProps) {
baseWatch(() => patchAttrs(instance), undefined, {
scheduler: createVaporPreScheduler(instance),
})
} else {
patchAttrs(instance)
}
if (isStateful) {
instance.props = shallowReactive(props)
instance.props = /* isSSR ? props : */ shallowReactive(props)
} else {
if (instance.propsOptions === EMPTY_ARR) {
instance.props = attrs
} else {
instance.props = props
// functional w/ optional props, props === attrs
instance.props = instance.propsOptions === EMPTY_ARR ? attrs : props
}
}
instance.attrs = attrs
}
return hasAttrsChanged
function registerProp(
instance: ComponentInternalInstance,
props: Data,
rawKey: string,
getter?: (() => unknown) | (() => DynamicPropResult),
isDynamic?: boolean,
isAbsent?: boolean,
) {
const key = camelize(rawKey)
if (key in props) return
const [options, needCastKeys] = instance.propsOptions
const needCast = needCastKeys && needCastKeys.includes(key)
const withCast = (value: unknown, absent?: boolean) =>
resolvePropValue(options!, props, key, value, instance, absent)
if (isAbsent) {
props[key] = needCast ? withCast(undefined, true) : undefined
} else {
const get: () => unknown = isDynamic
? needCast
? () => withCast(...(getter!() as DynamicPropResult))
: () => (getter!() as DynamicPropResult)[0]
: needCast
? () => withCast(getter!())
: getter!
Object.defineProperty(props, key, {
get,
enumerable: true,
})
}
}
function getRawKey(obj: Data, key: string) {
return Object.keys(obj).find(k => camelize(k) === key)
}
type DynamicPropResult = [value: unknown, absent: boolean]
function getDynamicPropValue(
rawProps: NormalizedRawProps,
key: string,
): DynamicPropResult {
for (const props of Array.from(rawProps).reverse()) {
if (isFunction(props)) {
const resolved = props()
const rawKey = getRawKey(resolved, key)
if (rawKey) return [resolved[rawKey], false]
} else {
const rawKey = getRawKey(props, key)
if (rawKey) return [props[rawKey](), false]
}
}
return [undefined, true]
}
function resolvePropValue(
@ -179,7 +199,7 @@ function resolvePropValue(
key: string,
value: unknown,
instance: ComponentInternalInstance,
isAbsent: boolean,
isAbsent?: boolean,
) {
const opt = options[key]
if (opt != null) {
@ -223,12 +243,12 @@ function resolvePropValue(
export function normalizePropsOptions(comp: Component): NormalizedPropsOptions {
// TODO: cahching?
const raw = comp.props as any
const normalized: NormalizedPropsOptions[0] = {}
const raw = comp.props
const normalized: NormalizedProps | undefined = {}
const needCastKeys: NormalizedPropsOptions[1] = []
if (!raw) {
return EMPTY_ARR as any
return EMPTY_ARR as []
}
if (isArray(raw)) {
@ -238,7 +258,7 @@ export function normalizePropsOptions(comp: Component): NormalizedPropsOptions {
normalized[normalizedKey] = EMPTY_OBJ
}
}
} else if (raw) {
} else {
for (const key in raw) {
const normalizedKey = camelize(key)
if (validatePropName(normalizedKey)) {
@ -267,6 +287,8 @@ export function normalizePropsOptions(comp: Component): NormalizedPropsOptions {
function validatePropName(key: string) {
if (key[0] !== '$') {
return true
} else if (__DEV__) {
warn(`Invalid prop name: "${key}" is a reserved property.`)
}
return false
}
@ -296,21 +318,24 @@ function getTypeIndex(
* dev only
*/
function validateProps(
rawProps: Data,
rawProps: NormalizedRawProps,
props: Data,
instance: ComponentInternalInstance,
options: NormalizedProps,
) {
const resolvedValues = toRaw(props)
const options = instance.propsOptions[0]
const presentKeys: string[] = []
for (const props of rawProps) {
presentKeys.push(...Object.keys(isFunction(props) ? props() : props))
}
for (const key in options) {
let opt = options[key]
if (opt == null) continue
const opt = options[key]
if (opt != null)
validateProp(
key,
resolvedValues[key],
props[key],
opt,
__DEV__ ? shallowReadonly(resolvedValues) : resolvedValues,
!hasOwn(rawProps, key) && !hasOwn(rawProps, hyphenate(key)),
props,
!presentKeys.some(k => camelize(k) === key),
)
}
}
@ -321,11 +346,11 @@ function validateProps(
function validateProp(
name: string,
value: unknown,
prop: PropOptions,
option: PropOptions,
props: Data,
isAbsent: boolean,
) {
const { required, validator } = prop
const { required, validator } = option
// required!
if (required && isAbsent) {
warn('Missing required prop: "' + name + '"')

View File

@ -145,7 +145,3 @@ function callDirectiveHook(
])
resetTracking()
}
export function resolveDirective() {
// TODO
}

View File

@ -1,5 +1,5 @@
import { isArray, toDisplayString } from '@vue/shared'
import type { Block } from '../render'
import type { Block } from '../apiRender'
/*! #__NO_SIDE_EFFECTS__ */
export function normalizeBlock(block: Block): Node[] {

View File

@ -0,0 +1,7 @@
export function resolveComponent() {
// TODO
}
export function resolveDirective() {
// TODO
}

View File

@ -46,7 +46,6 @@ export {
type FunctionalComponent,
type SetupFn,
} from './component'
export { render, unmountComponent } from './render'
export { renderEffect, renderWatch } from './renderWatch'
export {
watch,
@ -62,7 +61,6 @@ export {
} from './apiWatch'
export {
withDirectives,
resolveDirective,
type Directive,
type DirectiveBinding,
type DirectiveHook,
@ -88,7 +86,6 @@ export { on, delegate, delegateEvents, setDynamicEvents } from './dom/event'
export { setRef } from './dom/templateRef'
export { defineComponent } from './apiDefineComponent'
export { createComponentInstance } from './component'
export {
onBeforeMount,
onMounted,
@ -103,8 +100,17 @@ export {
onErrorCaptured,
// onServerPrefetch,
} from './apiLifecycle'
export {
createVaporApp,
type App,
type AppConfig,
type AppContext,
} from './apiCreateVaporApp'
export { createIf } from './apiCreateIf'
export { createFor } from './apiCreateFor'
export { createComponent } from './apiCreateComponent'
export { resolveComponent, resolveDirective } from './helpers/resolveAssets'
// **Internal** DOM-only runtime directive helpers
export {

View File

@ -120,9 +120,9 @@ watch(
files.value['src/index.html'] = new File(
'src/index.html',
`<script type="module">
import { render } from 'vue/vapor'
import { createVaporApp } from 'vue/vapor'
import App from './App.vue'
render(App, {}, '#app')` +
createVaporApp(App).mount('#app')` +
'<' +
'/script>' +
`<div id="app"></div>`,

View File

@ -0,0 +1,43 @@
<script setup lang="ts">
import {
onBeforeMount,
onMounted,
onBeforeUnmount,
onUnmounted,
ref,
} from 'vue/vapor'
import SubComp from './sub-comp.vue'
const bar = ref('update')
const id = ref('id')
const p = ref<any>({
bar,
id: 'not id',
test: 100,
})
function update() {
bar.value = 'updated'
p.value.foo = 'updated foo'
p.value.newAttr = 'new attr'
id.value = 'updated id'
}
function update2() {
delete p.value.test
}
onBeforeMount(() => console.log('root: before mount'))
onMounted(() => console.log('root: mounted'))
onBeforeUnmount(() => console.log('root: before unmount'))
onUnmounted(() => console.log('root: unmounted'))
</script>
<template>
<div>
root comp
<button @click="update">update</button>
<button @click="update2">update2</button>
<SubComp foo="foo" v-bind="p" :baz="'baz'" :id />
</div>
</template>

View File

@ -1,4 +1,4 @@
import { createComponentInstance, render, unmountComponent } from 'vue/vapor'
import { createVaporApp } from 'vue/vapor'
import { createApp } from 'vue'
import './style.css'
@ -6,14 +6,11 @@ const modules = import.meta.glob<any>('./**/*.(vue|js)')
const mod = (modules['.' + location.pathname] || modules['./App.vue'])()
mod.then(({ default: mod }) => {
if (mod.vapor) {
const instance = createComponentInstance(mod, {})
render(instance, '#app')
const app = (mod.vapor ? createVaporApp : createApp)(mod)
app.mount('#app')
// @ts-expect-error
globalThis.unmount = () => {
unmountComponent(instance)
}
} else {
createApp(mod).mount('#app')
app.unmount()
}
})

View File

@ -1,69 +1,53 @@
// @ts-check
import {
children,
createComponent,
defineComponent,
on,
reactive,
ref,
render as renderComponent,
renderEffect,
setText,
template,
watch,
watchEffect,
} from '@vue/vapor'
const t0 = template('<button></button>')
export default defineComponent({
vapor: true,
props: undefined,
setup(_, {}) {
setup() {
const count = ref(1)
const props = reactive({
a: 'b',
'foo-bar': 100,
})
const handleClick = () => {
count.value++
props['foo-bar']++
// @ts-expect-error
props.boolean = true
console.log(count)
}
const __returned__ = { count, handleClick }
Object.defineProperty(__returned__, '__isScriptSetup', {
enumerable: false,
value: true,
})
return __returned__
},
render(_ctx) {
const n0 = t0()
const n1 = /** @type {HTMLButtonElement} */ (children(n0, 0))
on(n1, 'click', () => _ctx.handleClick)
watchEffect(() => {
setText(n1, _ctx.count)
})
// TODO: create component fn?
// const c0 = createComponent(...)
// insert(n0, c0)
renderComponent(
/** @type {any} */ (child),
// TODO: proxy??
return (() => {
const n0 = /** @type {HTMLButtonElement} */ (t0())
on(n0, 'click', () => handleClick)
renderEffect(() => setText(n0, count.value))
/** @type {any} */
const n1 = createComponent(child, [
{
/* <Comp :count="count" /> */
get count() {
return _ctx.count
count: () => {
// console.trace('access')
return count.value
},
/* <Comp :inline-double="count * 2" /> */
get inlineDouble() {
return _ctx.count * 2
inlineDouble: () => count.value * 2,
id: () => 'hello',
},
},
// @ts-expect-error TODO
n0[0],
)
return n0
() => props,
])
return [n0, n1]
})()
},
})
@ -74,25 +58,22 @@ const child = defineComponent({
props: {
count: { type: Number, default: 1 },
inlineDouble: { type: Number, default: 2 },
fooBar: { type: Number, required: true },
boolean: { type: Boolean },
},
setup(props) {
watch(
() => props.count,
v => console.log('count changed', v),
)
watch(
() => props.inlineDouble,
v => console.log('inlineDouble changed', v),
)
},
setup(props, { attrs }) {
console.log(props, { ...props })
console.log(attrs, { ...attrs })
render(_ctx) {
const n0 = t1()
const n1 = children(n0, 0)
watchEffect(() => {
setText(n1, void 0, _ctx.count + ' * 2 = ' + _ctx.inlineDouble)
})
return n0
return (() => {
const n0 = /** @type {HTMLParagraphElement} */ (t1())
renderEffect(() =>
setText(n0, props.count + ' * 2 = ' + props.inlineDouble),
)
const n1 = /** @type {HTMLParagraphElement} */ (t1())
renderEffect(() => setText(n1, props.fooBar, ', ', props.boolean))
return [n0, n1]
})()
},
})

View File

@ -0,0 +1,36 @@
<script setup lang="ts">
import {
getCurrentInstance,
onBeforeMount,
onBeforeUnmount,
onMounted,
onUnmounted,
watchEffect,
} from 'vue/vapor'
const props = defineProps<{
foo: string
bar: string
baz: string
}>()
const attrs = getCurrentInstance()?.attrs
watchEffect(() => {
console.log({ ...attrs })
})
const keys = Object.keys
onBeforeMount(() => console.log('sub: before mount'))
onMounted(() => console.log('sub: mounted'))
onBeforeUnmount(() => console.log('sub: before unmount'))
onUnmounted(() => console.log('sub: unmounted'))
</script>
<template>
<div>sub-comp</div>
{{ props }}
{{ attrs }}
{{ keys(attrs) }}
</template>