diff --git a/packages/hydration/hydration.ts b/packages/hydration/hydration.ts index 6c4bfc68..ead42d19 100644 --- a/packages/hydration/hydration.ts +++ b/packages/hydration/hydration.ts @@ -1,38 +1,39 @@ -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 @@ -40,7 +41,7 @@ export function hydrateOnMediaQuery (framework: AsyncFrameworkFn, component: Asy const hydrate = () => { onChange() - resolveAndHydrate(framework, component, id, props, slots) + resolveAndHydrate(framework, component, id, props, slots, enhanceIslands) } mediaQuery.matches ? hydrate() : onChange(hydrate) @@ -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. @@ -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() @@ -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) \ No newline at end of file diff --git a/packages/hydration/types.ts b/packages/hydration/types.ts index ddf1e8d2..008e0ef2 100644 --- a/packages/hydration/types.ts +++ b/packages/hydration/types.ts @@ -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 diff --git a/packages/hydration/vue.ts b/packages/hydration/vue.ts index 1948c7dc..819e7de4 100644 --- a/packages/hydration/vue.ts +++ b/packages/hydration/vue.ts @@ -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)] })) @@ -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) @@ -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 -} +} \ No newline at end of file diff --git a/packages/iles/src/client/app/components/Island.vue b/packages/iles/src/client/app/components/Island.vue index d8978932..f95f64a4 100644 --- a/packages/iles/src/client/app/components/Island.vue +++ b/packages/iles/src/client/app/components/Island.vue @@ -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 const renderSlots = async () => @@ -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) ` } diff --git a/packages/iles/src/client/app/index.ts b/packages/iles/src/client/app/index.ts index d175c9e7..ba468875 100644 --- a/packages/iles/src/client/app/index.ts +++ b/packages/iles/src/client/app/index.ts @@ -17,7 +17,7 @@ import { resolveProps } from './props' const newApp = import.meta.env.SSR ? createSSRApp : createClientApp -function createRouter (base: string | undefined, routerOptions: Partial) { +function createRouter(base: string | undefined, routerOptions: Partial) { if (base === '/') base = undefined return createVueRouter({ @@ -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 diff --git a/packages/iles/types/shared.d.ts b/packages/iles/types/shared.d.ts index 5c23cf59..7ad55801 100644 --- a/packages/iles/types/shared.d.ts +++ b/packages/iles/types/shared.d.ts @@ -181,11 +181,16 @@ export interface IlesModule extends Partial { } export type EnhanceAppContext = AppContext +export type EnhanceApp = (ctx: EnhanceAppContext) => void | Promise +export type EnhanceIslandContext = IslandContext +export type EnhanceIslands = (ctx: EnhanceIslandContext) => void | Promise + export type MDXComponents = Record export interface UserApp { head?: HeadConfig | ((ctx: EnhanceAppContext) => HeadConfig) - enhanceApp?: (ctx: EnhanceAppContext) => void | Promise + enhanceApp?: EnhanceApp + enhanceIslands?: EnhanceIslands mdxComponents?: MDXComponents | ((ctx: EnhanceAppContext) => MDXComponents | Promise) router?: Omit socialTags?: boolean