Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: implement inheritAttrs #153

Merged
merged 34 commits into from
Mar 18, 2024
Merged
Changes from all commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
bb26087
feat(vapor): implement inheritAttrs
Doctor-wu Mar 17, 2024
7e1f592
feat(runtime-vapor): apply inheritAttrs to instance
Doctor-wu Mar 17, 2024
9d8c1e5
feat(runtime-vapor): extract apiSetup & init setup ctx
Doctor-wu Mar 17, 2024
20aa749
feat(runtime-vapor): add setup ctx type
Doctor-wu Mar 17, 2024
e4bc5ab
Revert "feat(runtime-vapor): add setup ctx type"
Doctor-wu Mar 17, 2024
2f8b9af
Revert "feat(runtime-vapor): extract apiSetup & init setup ctx"
Doctor-wu Mar 17, 2024
757aa74
feat(runtime-vapor): impl fallthrough attrs
Doctor-wu Mar 17, 2024
2db5e19
test(runtime-vapor): tweak props test case
Doctor-wu Mar 17, 2024
8498ba6
feat(runtime-vapor): update attrs when props update
Doctor-wu Mar 17, 2024
1742efe
feat(runtime-vapor): remove unecessary property & use proper way to s…
Doctor-wu Mar 18, 2024
e082299
feat(runtime-vapor, compiler-vapor): add withAttr in singleRoot Compo…
Doctor-wu Mar 18, 2024
5cab771
feat(runtime-vapor, compiler-vapor): resolve nest component attrs
Doctor-wu Mar 18, 2024
1482a20
feat(compiler-vapor): remove unecessary clone props
Doctor-wu Mar 18, 2024
b3b6622
feat(runtime-vapor): clean code
Doctor-wu Mar 18, 2024
791f533
chore(runtime-vapor): make lint happy
Doctor-wu Mar 18, 2024
f7d4c13
feat(runtime-vapor, compiler-vapor): make inheritAttrs reactive
Doctor-wu Mar 18, 2024
bdc1ee4
chore(runtime-vapor): remove unecessary code
Doctor-wu Mar 18, 2024
35499bd
feat(runtime-vapor): should not withAttrs when component set inheritA…
Doctor-wu Mar 18, 2024
a474ac4
feat(runtim-vapor, compiler-vapor): simplify implement
Doctor-wu Mar 18, 2024
24488bc
Merge branch 'main' into feature-inherit-attrs
Doctor-wu Mar 18, 2024
6044bf7
test(compiler-vapor): add generator component test case & snapshot
Doctor-wu Mar 18, 2024
8afe97d
Merge branch 'main' into feature-inherit-attrs
Doctor-wu Mar 18, 2024
c30a618
feat(runtime-vapor): simplify implement
Doctor-wu Mar 18, 2024
c085cda
feat(runtime-vapor): simplify implement
Doctor-wu Mar 18, 2024
0ea395c
refactor: optimize bundle size
sxzz Mar 18, 2024
b5879d3
refactor: check if component
sxzz Mar 18, 2024
322f0ba
refactor: remove type casting
sxzz Mar 18, 2024
34c1937
feat(compiler-vapor): optimize single root implemention
Doctor-wu Mar 18, 2024
56ff7c4
fix: type error
sxzz Mar 18, 2024
47754b7
feat(compiler-vapor): perf bundle size
Doctor-wu Mar 18, 2024
5270734
chore(compiler-vapor, runtime-vapor): bundle size & make lint happy
Doctor-wu Mar 18, 2024
c068506
refactor: simplify
sxzz Mar 18, 2024
61b572c
chore(compiler-vapor): remove unecessary import
Doctor-wu Mar 18, 2024
47f8b9a
test: undo
sxzz Mar 18, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html

exports[`generate component > generate multi root component 1`] = `
"import { resolveComponent as _resolveComponent, createComponent as _createComponent, template as _template } from 'vue/vapor';
const t0 = _template("123")
export function render(_ctx) {
const n1 = t0()
const n0 = _createComponent(_resolveComponent("Comp"))
return [n0, n1]
}"
`;

exports[`generate component > generate single root component (with props) 1`] = `
"import { resolveComponent as _resolveComponent, createComponent as _createComponent } from 'vue/vapor';
export function render(_ctx) {
const n0 = _createComponent(_resolveComponent("Comp"), [{
foo: () => (foo)
}], true)
return n0
}"
`;

exports[`generate component > generate single root component (without props) 1`] = `
"import { resolveComponent as _resolveComponent, createComponent as _createComponent } from 'vue/vapor';
export function render(_ctx) {
const n0 = _createComponent(_resolveComponent("Comp"), null, true)
return n0
}"
`;

exports[`generate component > should not generate withAttrs if component is not the root of the template 1`] = `
"import { resolveComponent as _resolveComponent, createComponent as _createComponent, insert as _insert, template as _template } from 'vue/vapor';
const t0 = _template("<div></div>")
export function render(_ctx) {
const n1 = t0()
const n0 = _createComponent(_resolveComponent("Comp"))
_insert(n0, n1)
return n1
}"
`;
23 changes: 23 additions & 0 deletions packages/compiler-vapor/__tests__/generators/component.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { compile } from '@vue/compiler-vapor'

describe('generate component', () => {
test('generate single root component (without props)', () => {
const { code } = compile(`<Comp/>`)
expect(code).toMatchSnapshot()
})

test('generate single root component (with props)', () => {
const { code } = compile(`<Comp :foo="foo"/>`)
expect(code).toMatchSnapshot()
})

test('generate multi root component', () => {
const { code } = compile(`<Comp/>123`)
expect(code).toMatchSnapshot()
})

test('should not generate withAttrs if component is not the root of the template', () => {
const { code } = compile(`<div><Comp/></div>`)
expect(code).toMatchSnapshot()
})
})
10 changes: 9 additions & 1 deletion packages/compiler-vapor/src/generators/component.ts
Original file line number Diff line number Diff line change
@@ -22,10 +22,18 @@ export function genCreateComponent(
? genCall(vaporHelper('resolveComponent'), JSON.stringify(oper.tag))
: [oper.tag]

const isRoot = oper.root
const props = genProps()

return [
NEWLINE,
`const n${oper.id} = `,
...genCall(vaporHelper('createComponent'), tag, genProps()),
...genCall(
vaporHelper('createComponent'),
tag,
props || (isRoot ? 'null' : false),
isRoot && 'true',
),
]

function genProps() {
1 change: 1 addition & 0 deletions packages/compiler-vapor/src/ir.ts
Original file line number Diff line number Diff line change
@@ -182,6 +182,7 @@ export interface CreateComponentIRNode extends BaseIRNode {
// TODO slots

resolve: boolean
root: boolean
}

export type IRNode = OperationNode | RootIRNode
3 changes: 3 additions & 0 deletions packages/compiler-vapor/src/transforms/transformElement.ts
Original file line number Diff line number Diff line change
@@ -64,13 +64,16 @@ function transformComponentElement(
const { bindingMetadata } = context.options
const resolve = !bindingMetadata[tag]
context.dynamic.flags |= DynamicFlag.NON_TEMPLATE | DynamicFlag.INSERT
const root =
context.root === context.parent && context.parent.node.children.length === 1

context.registerOperation({
type: IRNodeTypes.CREATE_COMPONENT_NODE,
id: context.reference(),
tag,
props: propsResult[0] ? propsResult[1] : [propsResult[1]],
resolve,
root,
})
}

163 changes: 163 additions & 0 deletions packages/runtime-vapor/__tests__/componentAttrs.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,163 @@
import {
createComponent,
getCurrentInstance,
nextTick,
ref,
setText,
template,
watchEffect,
} from '../src'
import { setCurrentInstance } from '../src/component'
import { makeRender } from './_utils'

const define = makeRender<any>()

describe('attribute fallthrough', () => {
it('should allow attrs to fallthrough', async () => {
const t0 = template('<div>')
const { component: Child } = define({
props: ['foo'],
render() {
const instance = getCurrentInstance()!
const n0 = t0()
watchEffect(() => setText(n0, instance.props.foo))
return n0
},
})

const foo = ref(1)
const id = ref('a')
const { instance, host } = define({
setup() {
return { foo, id }
},
render(_ctx: Record<string, any>) {
return createComponent(
Child,
[
{
foo: () => _ctx.foo,
id: () => _ctx.id,
},
],
true,
)
},
}).render()
const reset = setCurrentInstance(instance)
expect(host.innerHTML).toBe('<div id="a">1</div>')

foo.value++
await nextTick()
expect(host.innerHTML).toBe('<div id="a">2</div>')

id.value = 'b'
await nextTick()
expect(host.innerHTML).toBe('<div id="b">2</div>')
reset()
})

it('should not fallthrough if explicitly pass inheritAttrs: false', async () => {
const t0 = template('<div>')
const { component: Child } = define({
props: ['foo'],
inheritAttrs: false,
render() {
const instance = getCurrentInstance()!
const n0 = t0()
watchEffect(() => setText(n0, instance.props.foo))
return n0
},
})

const foo = ref(1)
const id = ref('a')
const { instance, host } = define({
setup() {
return { foo, id }
},
render(_ctx: Record<string, any>) {
return createComponent(
Child,
[
{
foo: () => _ctx.foo,
id: () => _ctx.id,
},
],
true,
)
},
}).render()
const reset = setCurrentInstance(instance)
expect(host.innerHTML).toBe('<div>1</div>')

foo.value++
await nextTick()
expect(host.innerHTML).toBe('<div>2</div>')

id.value = 'b'
await nextTick()
expect(host.innerHTML).toBe('<div>2</div>')
reset()
})

it('should pass through attrs in nested single root components', async () => {
const t0 = template('<div>')
const { component: Grandson } = define({
props: ['custom-attr'],
render() {
const instance = getCurrentInstance()!
const n0 = t0()
watchEffect(() => setText(n0, instance.attrs.foo))
return n0
},
})

const { component: Child } = define({
render() {
const n0 = createComponent(
Grandson,
[
{
'custom-attr': () => 'custom-attr',
},
],
true,
)
return n0
},
})

const foo = ref(1)
const id = ref('a')
const { instance, host } = define({
setup() {
return { foo, id }
},
render(_ctx: Record<string, any>) {
return createComponent(
Child,
[
{
foo: () => _ctx.foo,
id: () => _ctx.id,
},
],
true,
)
},
}).render()
const reset = setCurrentInstance(instance)
expect(host.innerHTML).toBe('<div foo="1" id="a">1</div>')

foo.value++
await nextTick()
expect(host.innerHTML).toBe('<div foo="2" id="a">2</div>')

id.value = 'b'
await nextTick()
expect(host.innerHTML).toBe('<div foo="2" id="b">2</div>')
reset()
})
})
21 changes: 12 additions & 9 deletions packages/runtime-vapor/__tests__/componentProps.spec.ts
Original file line number Diff line number Diff line change
@@ -238,24 +238,26 @@ describe('component: props', () => {
return { foo, id }
},
render(_ctx: Record<string, any>) {
return createComponent(Child, {
foo: () => _ctx.foo,
id: () => _ctx.id,
})
return createComponent(
Child,
{
foo: () => _ctx.foo,
id: () => _ctx.id,
},
true,
)
},
}).render()
const reset = setCurrentInstance(instance)
// expect(host.innerHTML).toBe('<div id="a">1</div>') // TODO: Fallthrough Attributes
expect(host.innerHTML).toBe('<div>1</div>')
expect(host.innerHTML).toBe('<div id="a">1</div>')

foo.value++
await nextTick()
// expect(host.innerHTML).toBe('<div id="a">2</div>') // TODO: Fallthrough Attributes
expect(host.innerHTML).toBe('<div>2</div>')
expect(host.innerHTML).toBe('<div id="a">2</div>')

id.value = 'b'
await nextTick()
// expect(host.innerHTML).toBe('<div id="b">2</div>') // TODO: Fallthrough Attributes
expect(host.innerHTML).toBe('<div id="b">2</div>')
reset()
})

@@ -441,6 +443,7 @@ describe('component: props', () => {
// #5016
test('handling attr with undefined value', () => {
const { render, host } = define({
inheritAttrs: false,
render() {
const instance = getCurrentInstance()!
const t0 = template('<div></div>')
16 changes: 12 additions & 4 deletions packages/runtime-vapor/src/apiCreateComponent.ts
Original file line number Diff line number Diff line change
@@ -5,14 +5,22 @@ import {
} from './component'
import { setupComponent } from './apiRender'
import type { RawProps } from './componentProps'
import { withAttrs } from './componentAttrs'

export function createComponent(comp: Component, rawProps: RawProps = null) {
export function createComponent(
comp: Component,
rawProps: RawProps | null = null,
singleRoot: boolean = false,
) {
const current = currentInstance!
const instance = createComponentInstance(comp, rawProps)
setupComponent(instance)
const instance = createComponentInstance(
comp,
singleRoot ? withAttrs(rawProps) : rawProps,
)
setupComponent(instance, singleRoot)

// register sub-component with current component for lifecycle management
current.comps.add(instance)

return instance.block
return instance
}
3 changes: 3 additions & 0 deletions packages/runtime-vapor/src/apiCreateFor.ts
Original file line number Diff line number Diff line change
@@ -4,6 +4,7 @@ import { createComment, createTextNode, insert, remove } from './dom/element'
import { renderEffect } from './renderEffect'
import { type Block, type Fragment, fragmentKey } from './apiRender'
import { warn } from './warning'
import { componentKey } from './component'

interface ForBlock extends Fragment {
scope: EffectScope
@@ -343,6 +344,8 @@ function normalizeAnchor(node: Block): Node {
return node
} else if (isArray(node)) {
return normalizeAnchor(node[0])
} else if (componentKey in node) {
return normalizeAnchor(node.block!)
} else {
return normalizeAnchor(node.nodes!)
}
23 changes: 17 additions & 6 deletions packages/runtime-vapor/src/apiRender.ts
Original file line number Diff line number Diff line change
@@ -1,21 +1,29 @@
import { isArray, isFunction, isObject } from '@vue/shared'
import { type ComponentInternalInstance, setCurrentInstance } from './component'
import {
type ComponentInternalInstance,
componentKey,
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'
import { fallThroughAttrs } from './componentAttrs'

export const fragmentKey = Symbol(__DEV__ ? `fragmentKey` : ``)

export type Block = Node | Fragment | Block[]
export type Block = Node | Fragment | ComponentInternalInstance | Block[]
export type Fragment = {
nodes: Block
anchor?: Node
[fragmentKey]: true
}

export function setupComponent(instance: ComponentInternalInstance): void {
export function setupComponent(
instance: ComponentInternalInstance,
singleRoot: boolean = false,
): void {
const reset = setCurrentInstance(instance)
instance.scope.run(() => {
const { component, props, emit, attrs } = instance
@@ -30,9 +38,10 @@ export function setupComponent(instance: ComponentInternalInstance): void {
stateOrNode &&
(stateOrNode instanceof Node ||
isArray(stateOrNode) ||
(stateOrNode as any)[fragmentKey])
fragmentKey in stateOrNode ||
componentKey in stateOrNode)
) {
block = stateOrNode as Block
block = stateOrNode
} else if (isObject(stateOrNode)) {
instance.setupState = proxyRefs(stateOrNode)
}
@@ -47,7 +56,9 @@ export function setupComponent(instance: ComponentInternalInstance): void {
// TODO: warn no template
block = []
}
return (instance.block = block)
instance.block = block
if (singleRoot) fallThroughAttrs(instance)
return block
})
reset()
}
5 changes: 5 additions & 0 deletions packages/runtime-vapor/src/component.ts
Original file line number Diff line number Diff line change
@@ -28,6 +28,7 @@ export type FunctionalComponent = SetupFn & Omit<ObjectComponent, 'setup'>

export interface ObjectComponent {
props?: ComponentPropsOptions
inheritAttrs?: boolean
emits?: EmitsOptions
setup?: SetupFn
render?(ctx: any): Block
@@ -36,7 +37,10 @@ export interface ObjectComponent {

type LifecycleHook<TFn = Function> = TFn[] | null

export const componentKey = Symbol(__DEV__ ? `componentKey` : ``)

export interface ComponentInternalInstance {
[componentKey]: true
uid: number
vapor: true

@@ -143,6 +147,7 @@ export function createComponentInstance(
rawProps: RawProps | null,
): ComponentInternalInstance {
const instance: ComponentInternalInstance = {
[componentKey]: true,
uid: uid++,
vapor: true,

30 changes: 28 additions & 2 deletions packages/runtime-vapor/src/componentAttrs.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
import { camelize, isFunction } from '@vue/shared'
import type { ComponentInternalInstance } from './component'
import { camelize, isArray, isFunction } from '@vue/shared'
import { type ComponentInternalInstance, currentInstance } from './component'
import { isEmitListener } from './componentEmits'
import { setDynamicProps } from './dom/prop'
import type { RawProps } from './componentProps'
import { renderEffect } from './renderEffect'

export function patchAttrs(instance: ComponentInternalInstance) {
const attrs = instance.attrs
@@ -42,3 +45,26 @@ export function patchAttrs(instance: ComponentInternalInstance) {
}
}
}

export function withAttrs(props: RawProps): RawProps {
const instance = currentInstance!
if (instance.component.inheritAttrs === false) return props
const attrsGetter = () => instance.attrs
if (!props) return [attrsGetter]
if (isArray(props)) {
return [attrsGetter, ...props]
}
return [attrsGetter, props]
}

export function fallThroughAttrs(instance: ComponentInternalInstance) {
const {
block,
component: { inheritAttrs },
} = instance
if (inheritAttrs === false) return

if (block instanceof Element) {
renderEffect(() => setDynamicProps(block, instance.attrs))
}
}
3 changes: 3 additions & 0 deletions packages/runtime-vapor/src/dom/element.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { isArray, toDisplayString } from '@vue/shared'
import type { Block } from '../apiRender'
import { componentKey } from '../component'

/*! #__NO_SIDE_EFFECTS__ */
export function normalizeBlock(block: Block): Node[] {
@@ -8,6 +9,8 @@ export function normalizeBlock(block: Block): Node[] {
nodes.push(block)
} else if (isArray(block)) {
block.forEach(child => nodes.push(...normalizeBlock(child)))
} else if (componentKey in block) {
nodes.push(...normalizeBlock(block.block!))
} else if (block) {
nodes.push(...normalizeBlock(block.nodes))
block.anchor && nodes.push(block.anchor)