Skip to content
Closed
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
25 changes: 13 additions & 12 deletions packages/hydration/hydration.ts
Original file line number Diff line number Diff line change
@@ -1,46 +1,47 @@
import { AsyncFrameworkFn, FrameworkFn, Component, AsyncComponent, Props, Slots } from './types'
import type { AsyncComponent, AsyncFrameworkFn, Component, EnhanceIslands, FrameworkFn, Props, Slots } from './types'

export { Framework, Props, Slots } from './types'

const findById = (id: string) =>
document.getElementById(id) || console.error(`Missing #${id}, could not mount island.`)

// Public: Hydrates the component immediately.
export function hydrateNow (framework: FrameworkFn, component: Component, id: string, props: Props, slots: Slots) {
export function hydrateNow (framework: FrameworkFn, component: Component, id: string, props: Props, slots: Slots, enhanceIslands: EnhanceIslands) {
const el = findById(id)
if (el) {
framework(component, id, el, props, slots)
framework(component, id, el, props, slots, enhanceIslands)
el.setAttribute('hydrated', '')
}
}

async function resolveAndHydrate (frameworkFn: AsyncFrameworkFn, componentFn: AsyncComponent, id: string, props: Props, slots: Slots) {
async function resolveAndHydrate (frameworkFn: AsyncFrameworkFn, componentFn: AsyncComponent, id: string, props: Props, slots: Slots, enhanceIslands: EnhanceIslands) {
const [framework, component] = await Promise.all([frameworkFn(), componentFn()])
hydrateNow(framework, component, id, props, slots)
hydrateNow(framework, component, id, props, slots, enhanceIslands)
}

// Public: Hydrate this component as soon as the main thread is free.
// If `requestIdleCallback` isn't supported, it uses a small delay.
export function hydrateWhenIdle (framework: AsyncFrameworkFn, component: AsyncComponent, id: string, props: Props, slots: Slots) {
export function hydrateWhenIdle (framework: AsyncFrameworkFn, component: AsyncComponent, id: string, props: Props, slots: Slots, enhanceIslands: EnhanceIslands) {
const whenIdle = window.requestIdleCallback || setTimeout
const cancelIdle = window.cancelIdleCallback || clearTimeout

const idleId: any = whenIdle(() =>
resolveAndHydrate(framework, component, id, props, slots))
resolveAndHydrate(framework, component, id, props, slots, enhanceIslands))

if (import.meta.env.DISPOSE_ISLANDS)
onDispose(id, () => cancelIdle(idleId))
}

// Public: Hydrate this component when the specified media query is matched.
export function hydrateOnMediaQuery (framework: AsyncFrameworkFn, component: AsyncComponent, id: string, props: Props, slots: Slots) {
export function hydrateOnMediaQuery (framework: AsyncFrameworkFn, component: AsyncComponent, id: string, props: Props, slots: Slots, enhanceIslands: EnhanceIslands) {
const mediaQuery = matchMedia(props._mediaQuery as string)
delete props._mediaQuery

const onChange = (fn: any = null) => mediaQuery.onchange = fn

const hydrate = () => {
onChange()
resolveAndHydrate(framework, component, id, props, slots)
resolveAndHydrate(framework, component, id, props, slots, enhanceIslands)
}

mediaQuery.matches ? hydrate() : onChange(hydrate)
Expand All @@ -50,7 +51,7 @@ export function hydrateOnMediaQuery (framework: AsyncFrameworkFn, component: Asy
}

// Public: Hydrate this component when one of it's children becomes visible.
export function hydrateWhenVisible (framework: AsyncFrameworkFn, component: AsyncComponent, id: string, props: Props, slots: Slots) {
export function hydrateWhenVisible (framework: AsyncFrameworkFn, component: AsyncComponent, id: string, props: Props, slots: Slots, enhanceIslands: EnhanceIslands) {
const el = findById(id)
if (el) {
// NOTE: Force detection of the element for non-Vue frameworks.
Expand All @@ -65,7 +66,7 @@ export function hydrateWhenVisible (framework: AsyncFrameworkFn, component: Asyn
if (import.meta.env.DEV)
el.style.display = ''

resolveAndHydrate(framework, component, id, props, slots)
resolveAndHydrate(framework, component, id, props, slots, enhanceIslands)
}
})
const stopObserver = () => observer.disconnect()
Expand All @@ -79,4 +80,4 @@ export function hydrateWhenVisible (framework: AsyncFrameworkFn, component: Asyn

// Internal: Invoked before navigation when turbo is enabled, or before HMR.
export const onDispose = (id: string, fn: () => void) =>
(window as any).__ILE_DISPOSE__?.set(id, fn)
(window as any).__ILE_DISPOSE__?.set(id, fn)
2 changes: 2 additions & 0 deletions packages/hydration/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

import type Vue from './vue'

export type { EnhanceIslands } from '../iles/types/shared'

export type Framework = 'vue' | 'preact' | 'solid' | 'svelte' | 'vanilla'
export type FrameworkFn = typeof Vue
export type AsyncFrameworkFn = () => Promise<FrameworkFn>
Expand Down
13 changes: 8 additions & 5 deletions packages/hydration/vue.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
import { h, createApp as createClientApp, createStaticVNode, createSSRApp } from 'vue'
import type { DefineComponent as Component, Component as App } from 'vue'
import type { Props, Slots } from './types'
import { createApp as createClientApp, createSSRApp, createStaticVNode, h } from 'vue'
import type { Component as App, DefineComponent as Component } from 'vue'
import type { EnhanceIslands, Props, Slots } from './types'
import { onDispose } from './hydration'

const createVueApp = import.meta.env.SSR ? createSSRApp : createClientApp

// Internal: Creates a Vue app and mounts it on the specified island root.
export default function createVueIsland (component: Component, id: string, el: Element, props: Props, slots: Slots | undefined) {
export default async function createVueIsland (component: Component, id: string, el: Element, props: Props, slots: Slots | undefined, enhanceIslands: EnhanceIslands) {
const slotFns = slots && Object.fromEntries(Object.entries(slots).map(([slotName, content]) => {
return [slotName, () => (createStaticVNode as any)(content)]
}))
Expand All @@ -17,6 +17,9 @@ export default function createVueIsland (component: Component, id: string, el: E
appDefinition.name = `Island: ${nameFromFile(component.__file)}`

const app = createVueApp(appDefinition)
if (enhanceIslands)
await enhanceIslands({ app })

app.mount(el!, Boolean(slots))

if (import.meta.env.DISPOSE_ISLANDS)
Expand All @@ -29,4 +32,4 @@ export default function createVueIsland (component: Component, id: string, el: E
function nameFromFile (file?: string) {
const regex = /(\w+?)(?:\.vue)?$/
return file?.match(regex)?.[1] || file
}
}
6 changes: 5 additions & 1 deletion packages/iles/src/client/app/components/Island.vue
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,7 @@ export default defineComponent({
const { _, ...slots } = this.$slots
const slotVNodes = mapObject(slots, slotFn => slotFn?.())
const hydrationPkg = `${isSSR ? '' : '/@id/'}@islands/hydration`
const userAppPkg = `${isSSR ? '@islands/user-app' : '/@id/virtual:user-app'}`
let renderedSlots: Record<string, string>

const renderSlots = async () =>
Expand All @@ -94,13 +95,16 @@ export default defineComponent({
const frameworkPath = `${hydrationPkg}/${this.framework}`

return `import { ${hydrationFns[this.strategy]} as hydrate } from '${hydrationPkg}'
import userApp from '${userAppPkg}'

const { enhanceIslands } = userApp
${isEager(this.strategy)
? `import framework from '${frameworkPath}'
import { ${this.importName} as component } from '${componentPath}'`
: `const framework = async () => (await import('${frameworkPath}')).default
const component = async () => (await import('${componentPath}')).${this.importName}`
}
hydrate(framework, component, '${this.id}', ${serialize(props)}, ${serialize(slots)})
hydrate(framework, component, '${this.id}', ${serialize(props)}, ${serialize(slots)}, enhanceIslands)
`
}

Expand Down
11 changes: 9 additions & 2 deletions packages/iles/src/client/app/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ import { resolveProps } from './props'

const newApp = import.meta.env.SSR ? createSSRApp : createClientApp

function createRouter (base: string | undefined, routerOptions: Partial<RouterOptions>) {
function createRouter(base: string | undefined, routerOptions: Partial<RouterOptions>) {
if (base === '/') base = undefined

return createVueRouter({
Expand Down Expand Up @@ -78,7 +78,14 @@ export const createApp: CreateAppFactory = async (options = {}) => {
// Apply any configuration added by the user in app.ts
// if (headConfig) useHead(ref(typeof headConfig === 'function' ? headConfig(context) : headConfig))
if (headConfig) head.push(ref(typeof headConfig === 'function' ? headConfig(context) : headConfig))
if (enhanceApp) await enhanceApp(context)

// enhanceIslands is called on the shell app during development otherwise user will have to duplicate `app.use(pinia)` in both enhanceIslands and enhanceApp
if (enhanceIslands) {
await enhanceIslands({ app })
}
if (enhanceApp) {
await enhanceApp(context)
}
await installMDXComponents(context, userApp)

return context
Expand Down
7 changes: 6 additions & 1 deletion packages/iles/types/shared.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -181,11 +181,16 @@ export interface IlesModule extends Partial<BaseIlesConfig> {
}

export type EnhanceAppContext = AppContext
export type EnhanceApp = (ctx: EnhanceAppContext) => void | Promise<void>
export type EnhanceIslandContext = IslandContext
export type EnhanceIslands = (ctx: EnhanceIslandContext) => void | Promise<void>

export type MDXComponents = Record<string, any>

export interface UserApp {
head?: HeadConfig | ((ctx: EnhanceAppContext) => HeadConfig)
enhanceApp?: (ctx: EnhanceAppContext) => void | Promise<void>
enhanceApp?: EnhanceApp
enhanceIslands?: EnhanceIslands
mdxComponents?: MDXComponents | ((ctx: EnhanceAppContext) => MDXComponents | Promise<MDXComponents>)
router?: Omit<VueRouterOptions, 'history', 'routes'>
socialTags?: boolean
Expand Down