Skip to content

feat(vapor): vapor keepalive #13186

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

Open
wants to merge 22 commits into
base: vapor
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
87 changes: 87 additions & 0 deletions packages-private/vapor-e2e-test/__tests__/keepalive.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
import path from 'node:path'
import {
E2E_TIMEOUT,
setupPuppeteer,
} from '../../../packages/vue/__tests__/e2e/e2eUtils'
import connect from 'connect'
import sirv from 'sirv'
const { page, html, click, value, enterValue } = setupPuppeteer()

describe('vapor keepalive', () => {
let server: any
const port = '8196'
beforeAll(() => {
server = connect()
.use(sirv(path.resolve(import.meta.dirname, '../dist')))
.listen(port)
process.on('SIGTERM', () => server && server.close())
})

beforeEach(async () => {
const baseUrl = `http://localhost:${port}/keepalive/`
await page().goto(baseUrl)
await page().waitForSelector('#app')
})

afterAll(() => {
server.close()
})

test(
'render vdom component',
async () => {
const testSelector = '.render-vdom-component'
const btnShow = `${testSelector} .btn-show`
const btnToggle = `${testSelector} .btn-toggle`
const container = `${testSelector} > div`
const inputSelector = `${testSelector} input`

let calls = await page().evaluate(() => {
return (window as any).getCalls()
})
expect(calls).toStrictEqual(['mounted', 'activated'])

expect(await html(container)).toBe('<input type="text">')
expect(await value(inputSelector)).toBe('vdom')

// change input value
await enterValue(inputSelector, 'changed')
expect(await value(inputSelector)).toBe('changed')

// deactivate
await click(btnToggle)
expect(await html(container)).toBe('')
calls = await page().evaluate(() => {
return (window as any).getCalls()
})
expect(calls).toStrictEqual(['deactivated'])

// activate
await click(btnToggle)
expect(await html(container)).toBe('<input type="text">')
expect(await value(inputSelector)).toBe('changed')
calls = await page().evaluate(() => {
return (window as any).getCalls()
})
expect(calls).toStrictEqual(['activated'])

// unmount keepalive
await click(btnShow)
expect(await html(container)).toBe('')
calls = await page().evaluate(() => {
return (window as any).getCalls()
})
expect(calls).toStrictEqual(['deactivated', 'unmounted'])

// mount keepalive
await click(btnShow)
expect(await html(container)).toBe('<input type="text">')
expect(await value(inputSelector)).toBe('vdom')
calls = await page().evaluate(() => {
return (window as any).getCalls()
})
expect(calls).toStrictEqual(['mounted', 'activated'])
},
E2E_TIMEOUT,
)
})
97 changes: 80 additions & 17 deletions packages-private/vapor-e2e-test/__tests__/vdomInterop.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,28 +6,31 @@ import {
import connect from 'connect'
import sirv from 'sirv'

describe('vdom / vapor interop', () => {
const { page, click, text, enterValue } = setupPuppeteer()

let server: any
const port = '8193'
beforeAll(() => {
server = connect()
.use(sirv(path.resolve(import.meta.dirname, '../dist')))
.listen(port)
process.on('SIGTERM', () => server && server.close())
})
const { page, click, html, value, text, enterValue } = setupPuppeteer()

let server: any
const port = '8193'
beforeAll(() => {
server = connect()
.use(sirv(path.resolve(import.meta.dirname, '../dist')))
.listen(port)
process.on('SIGTERM', () => server && server.close())
})

afterAll(() => {
server.close()
})
afterAll(() => {
server.close()
})

beforeEach(async () => {
const baseUrl = `http://localhost:${port}/interop/`
await page().goto(baseUrl)
await page().waitForSelector('#app')
})

describe('vdom / vapor interop', () => {
test(
'should work',
async () => {
const baseUrl = `http://localhost:${port}/interop/`
await page().goto(baseUrl)

expect(await text('.vapor > h2')).toContain('Vapor component in VDOM')

expect(await text('.vapor-prop')).toContain('hello')
Expand Down Expand Up @@ -81,4 +84,64 @@ describe('vdom / vapor interop', () => {
},
E2E_TIMEOUT,
)

describe('keepalive', () => {
test(
'render vapor component',
async () => {
const testSelector = '.render-vapor-component'
const btnShow = `${testSelector} .btn-show`
const btnToggle = `${testSelector} .btn-toggle`
const container = `${testSelector} > div`
const inputSelector = `${testSelector} input`

let calls = await page().evaluate(() => {
return (window as any).getCalls()
})
expect(calls).toStrictEqual(['mounted', 'activated'])

expect(await html(container)).toBe('<input type="text">')
expect(await value(inputSelector)).toBe('vapor')

// change input value
await enterValue(inputSelector, 'changed')
expect(await value(inputSelector)).toBe('changed')

// deactivate
await click(btnToggle)
expect(await html(container)).toBe('<!---->')
calls = await page().evaluate(() => {
return (window as any).getCalls()
})
expect(calls).toStrictEqual(['deactivated'])

// activate
await click(btnToggle)
expect(await html(container)).toBe('<input type="text">')
expect(await value(inputSelector)).toBe('changed')
calls = await page().evaluate(() => {
return (window as any).getCalls()
})
expect(calls).toStrictEqual(['activated'])

// unmount keepalive
await click(btnShow)
expect(await html(container)).toBe('<!---->')
calls = await page().evaluate(() => {
return (window as any).getCalls()
})
expect(calls).toStrictEqual(['deactivated', 'unmounted'])

// mount keepalive
await click(btnShow)
expect(await html(container)).toBe('<input type="text">')
expect(await value(inputSelector)).toBe('vapor')
calls = await page().evaluate(() => {
return (window as any).getCalls()
})
expect(calls).toStrictEqual(['mounted', 'activated'])
},
E2E_TIMEOUT,
)
})
})
1 change: 1 addition & 0 deletions packages-private/vapor-e2e-test/index.html
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
<a href="/interop/">VDOM / Vapor interop</a>
<a href="/todomvc/">Vapor TodoMVC</a>
<a href="/keepalive/">Vapor KeepAlive</a>
23 changes: 23 additions & 0 deletions packages-private/vapor-e2e-test/interop/App.vue
Original file line number Diff line number Diff line change
@@ -1,9 +1,20 @@
<script setup lang="ts">
import { ref } from 'vue'
import VaporComp from './VaporComp.vue'
import SimpleVaporComp from './components/SimpleVaporComp.vue'

const msg = ref('hello')
const passSlot = ref(true)

;(window as any).calls = []
;(window as any).getCalls = () => {
const ret = (window as any).calls.slice()
;(window as any).calls = []
return ret
}

const show = ref(true)
const toggle = ref(true)
</script>

<template>
Expand All @@ -19,4 +30,16 @@ const passSlot = ref(true)

<template #test v-if="passSlot">A test slot</template>
</VaporComp>

<!-- keepalive -->
<div class="render-vapor-component">
<button class="btn-show" @click="show = !show">show</button>
<button class="btn-toggle" @click="toggle = !toggle">toggle</button>
<div>
<KeepAlive v-if="show">
<SimpleVaporComp v-if="toggle" />
</KeepAlive>
</div>
</div>
<!-- keepalive end -->
</template>
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
<script vapor>
import { onActivated, onDeactivated, onMounted, onUnmounted, ref } from 'vue'
const msg = ref('vapor')

onMounted(() => {
window.calls.push('mounted')
})
onActivated(() => {
window.calls.push('activated')
})
onDeactivated(() => {
window.calls.push('deactivated')
})
onUnmounted(() => {
window.calls.push('unmounted')
})
</script>
<template>
<input type="text" v-model="msg" />
</template>
26 changes: 26 additions & 0 deletions packages-private/vapor-e2e-test/keepalive/App.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
<script vapor>
import { ref } from 'vue'
import VdomComp from './components/VdomComp.vue'

window.calls = []
window.getCalls = () => {
const ret = window.calls.slice()
window.calls = []
return ret
}

const show = ref(true)
const toggle = ref(true)
</script>

<template>
<div class="render-vdom-component">
<button class="btn-show" @click="show = !show">show</button>
<button class="btn-toggle" @click="toggle = !toggle">toggle</button>
<div>
<KeepAlive v-if="show">
<VdomComp v-if="toggle"></VdomComp>
</KeepAlive>
</div>
</div>
</template>
20 changes: 20 additions & 0 deletions packages-private/vapor-e2e-test/keepalive/components/VdomComp.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
<script setup>
import { onActivated, onDeactivated, onMounted, onUnmounted, ref } from 'vue'
const msg = ref('vdom')

onMounted(() => {
window.calls.push('mounted')
})
onActivated(() => {
window.calls.push('activated')
})
onDeactivated(() => {
window.calls.push('deactivated')
})
onUnmounted(() => {
window.calls.push('unmounted')
})
</script>
<template>
<input type="text" v-model="msg" />
</template>
2 changes: 2 additions & 0 deletions packages-private/vapor-e2e-test/keepalive/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
<script type="module" src="./main.ts"></script>
<div id="app"></div>
4 changes: 4 additions & 0 deletions packages-private/vapor-e2e-test/keepalive/main.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
import { createVaporApp, vaporInteropPlugin } from 'vue'
import App from './App.vue'

createVaporApp(App).use(vaporInteropPlugin).mount('#app')
1 change: 1 addition & 0 deletions packages-private/vapor-e2e-test/vite.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ export default defineConfig({
input: {
interop: resolve(import.meta.dirname, 'interop/index.html'),
todomvc: resolve(import.meta.dirname, 'todomvc/index.html'),
keepalive: resolve(import.meta.dirname, 'keepalive/index.html'),
},
},
},
Expand Down
10 changes: 9 additions & 1 deletion packages/compiler-vapor/src/generators/component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ import { genEventHandler } from './event'
import { genDirectiveModifiers, genDirectivesForElement } from './directive'
import { genBlock } from './block'
import { genModelHandler } from './vModel'
import { isBuiltInComponent } from '../utils'

export function genCreateComponent(
operation: CreateComponentIRNode,
Expand Down Expand Up @@ -92,8 +93,15 @@ export function genCreateComponent(
} else if (operation.asset) {
return toValidAssetId(operation.tag, 'component')
} else {
const { tag } = operation
const builtInTag = isBuiltInComponent(tag)
if (builtInTag) {
// @ts-expect-error
helper(builtInTag)
return `_${builtInTag}`
}
return genExpression(
extend(createSimpleExpression(operation.tag, false), { ast: null }),
extend(createSimpleExpression(tag, false), { ast: null }),
context,
)
}
Expand Down
8 changes: 7 additions & 1 deletion packages/compiler-vapor/src/transforms/transformElement.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ import {
type VaporDirectiveNode,
} from '../ir'
import { EMPTY_EXPRESSION } from './utils'
import { findProp } from '../utils'
import { findProp, isBuiltInComponent } from '../utils'

export const isReservedProp: (key: string) => boolean = /*#__PURE__*/ makeMap(
// the leading comma is intentional so empty string "" is also included
Expand Down Expand Up @@ -109,6 +109,12 @@ function transformComponentElement(
asset = false
}

const builtInTag = isBuiltInComponent(tag)
if (builtInTag) {
tag = builtInTag
asset = false
}

const dotIndex = tag.indexOf('.')
if (dotIndex > 0) {
const ns = resolveSetupReference(tag.slice(0, dotIndex), context)
Expand Down
11 changes: 11 additions & 0 deletions packages/compiler-vapor/src/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -88,3 +88,14 @@ export function getLiteralExpressionValue(
}
return exp.isStatic ? exp.content : null
}

export function isKeepAliveTag(tag: string): boolean {
tag = tag.toLowerCase()
return tag === 'keepalive' || tag === 'vaporkeepalive'
}

export function isBuiltInComponent(tag: string): string | undefined {
if (isKeepAliveTag(tag)) {
return 'VaporKeepAlive'
}
}
Loading