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
77 changes: 60 additions & 17 deletions docs/src/pages/config/index.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ sidebar: auto
[documents]: /guide/documents
[drafts]: /guide/markdown
[useDocuments]: /guide/documents
[discussion]: https://github.com/ElMassimo/iles/discussions/6#discussioncomment-1479755

# Configuration

Expand Down Expand Up @@ -158,16 +159,23 @@ Whether to output more information about islands and hydration in development.

## Your App

<Iles/> will pre-configure a Vue 3 app that will load any [pages] defined in the
site.
<Iles/> will pre-configure a Vue 3 shell app to enhance your development experience that will load any [pages] defined in your site.

You may provide additional configuration in `src/app.ts`, and leverage
intellisense by using the `defineApp` helper.
Note that this "outer" shell app is used only during development and is not available once your site is built.

You may provide additional configuration in `src/app.ts` by using the `defineApp` helper. These additional configuration can be for the shell app to customise your development/build logic, as well as for your whole site and the islands within your site.

```ts
import { defineApp } from 'iles'

export default defineApp({
enhanceApp ({ app, head, router }) {
// Configure the shell app to customise with development/build logic
},
enhanceIslands ({ app }) {
// Configure all Vue Islands to add Vue plugins
app.use(pinia)
},
head ({ frontmatter, site }) {
return {
meta: [
Expand All @@ -176,14 +184,56 @@ export default defineApp({
]
}
},
enhanceApp ({ app, head, router }) {
// Configure the app to add plugins.
},
router: {
scrollBehavior (current, previous, savedPosition) {
// Configure the scroll behavior
},
},
mdxComponents: {
b: 'strong',
img: Image,
},
socialTags: true // default
})
```

### `enhanceApp` (Development only)

- **Type:** `(context: AppContext) => void | Promise<void>`

A hook where you can add additional development/build logic. See this [discussion] thread for few suggestions.

### `enhanceIslands` (Vue Islands only)

- **Type:** `(context: IslandContext) => void | Promise<void>`

A hook where you can extend all your Vue Islands in your site with plugins such as pinia, i18n, vuetify, etc.

The hook will be invoked for every Vue Island in your site.

```ts
import { defineApp } from 'iles'
import { createI18n } from 'vue-i18n'
import { createPinia } from 'pinia'
import { createVuetify } from 'vuetify'

const i18n = createI18n({
// vue-i18n options ...
})
const pinia = createPinia()
const vuetify = createVuetify({
// vuetify options ...
})

export default defineApp({
enhanceIslands({ app }) {
app.use(i18n)
app.use(pinia)
if(app._component.name === 'Island: ChatboxIsland') {
// To initialise Vuetify only within ChatboxIsland.vue
app.use(vuetify)
}
},
})
```

Expand All @@ -193,12 +243,11 @@ export default defineApp({

Set the page title, meta tags, additional CSS, or scripts using [`@unhead/vue`][@unhead/vue].

### `enhanceApp`
### `router`

- **Type:** `(context: AppContext) => Promise<void>`
- **Type:** `RouterOptions`

A hook where you can add plugins to the Vue app, or do anything else prior to
the app being mounted.
Configure [`vue-router`][vue-router] by providing options such as `scrollBehavior` and `linkActiveClass`.

### `mdxComponents`

Expand All @@ -218,12 +267,6 @@ export default defineApp({
})
```

### `router`

- **Type:** `RouterOptions`

Configure [`vue-router`][vue-router] by providing options such as `scrollBehavior` and `linkActiveClass`.

### `socialTags`

- **Type:** `boolean`
Expand Down
23 changes: 12 additions & 11 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 Down
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
11 changes: 7 additions & 4 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 Down
4 changes: 3 additions & 1 deletion packages/iles/src/client/app/components/Island.vue
Original file line number Diff line number Diff line change
Expand Up @@ -94,13 +94,15 @@ export default defineComponent({
const frameworkPath = `${hydrationPkg}/${this.framework}`

return `import { ${hydrationFns[this.strategy]} as hydrate } from '${hydrationPkg}'
import userApp from '/@id/virtual:user-app'
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
8 changes: 8 additions & 0 deletions packages/iles/src/node/alias.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,9 @@ export const APP_CONFIG_REQUEST_PATH = `/${APP_CONFIG_ID}`
export const USER_APP_ID = '@islands/user-app'
export const USER_APP_REQUEST_PATH = `/${USER_APP_ID}`

export const USER_APP_ENHANCE_ISLANDS = 'virtual:user-app'
export const USER_APP_ENHANCE_ISLANDS_RESOLVED = `\0${USER_APP_ENHANCE_ISLANDS}`

export const USER_SITE_ID = '@islands/user-site'
export const USER_SITE_REQUEST_PATH = `/${USER_SITE_ID}`

Expand All @@ -40,6 +43,7 @@ export function resolveAliases (root: string, userConfig: UserConfig): AliasOpti
const paths: Record<string, string> = {
'/@shared': SHARED_PATH,
[USER_APP_ID]: USER_APP_REQUEST_PATH,
[USER_APP_ENHANCE_ISLANDS]: USER_APP_ENHANCE_ISLANDS_RESOLVED,
[USER_SITE_ID]: USER_SITE_REQUEST_PATH,
[APP_CONFIG_ID]: APP_CONFIG_REQUEST_PATH,
}
Expand Down Expand Up @@ -74,6 +78,10 @@ export function resolveAliases (root: string, userConfig: UserConfig): AliasOpti
'vue-router/dist/vue-router.esm-bundler.js',
),
},
{
find: /^\/@id\/virtual:user-app$/,
replacement: require.resolve(`${resolve(root, srcDir)}/app.ts`),
},
{
find: /^@islands\/hydration$/,
replacement: require.resolve('@islands/hydration'),
Expand Down
1 change: 1 addition & 0 deletions packages/iles/src/node/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -310,6 +310,7 @@ function viteConfigDefaults (root: string, userConfig: UserConfig): ViteOptions
'@islands/hydration',
'@islands/prerender',
'vue/server-renderer',
'virtual:user-app',
],
},
}
Expand Down
16 changes: 15 additions & 1 deletion packages/iles/src/node/plugin/plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import MagicString from 'magic-string'

import type { AppConfig, AppClientConfig } from '../shared'
import { ILES_APP_ENTRY } from '../constants'
import { APP_PATH, APP_COMPONENT_PATH, USER_APP_REQUEST_PATH, USER_SITE_REQUEST_PATH, APP_CONFIG_REQUEST_PATH, NOT_FOUND_COMPONENT_PATH, NOT_FOUND_REQUEST_PATH, DEBUG_COMPONENT_PATH } from '../alias'
import { APP_COMPONENT_PATH, APP_CONFIG_REQUEST_PATH, APP_PATH, DEBUG_COMPONENT_PATH, NOT_FOUND_COMPONENT_PATH, NOT_FOUND_REQUEST_PATH, USER_APP_ENHANCE_ISLANDS, USER_APP_ENHANCE_ISLANDS_RESOLVED, USER_APP_REQUEST_PATH, USER_SITE_REQUEST_PATH } from '../alias'
import { configureMiddleware } from './middleware'
import { serialize, pascalCase, exists, debug } from './utils'
import { parseId } from './parse'
Expand Down Expand Up @@ -149,6 +149,20 @@ export default function IslandsPlugins (appConfig: AppConfig): PluginOption[] {
return wrapLayout(code, path)
},
},
{
name: 'iles:enhance-islands',
resolveId(id) {
if (id === USER_APP_ENHANCE_ISLANDS) {
return USER_APP_ENHANCE_ISLANDS_RESOLVED
}
},
load(id) {
if (id === USER_APP_ENHANCE_ISLANDS_RESOLVED) {
return `import userApp from "${appPath.replace('.ts', '')}"
export default userApp`
}
},
},

plugins.vue,
...appConfig.vitePlugins,
Expand Down
10 changes: 9 additions & 1 deletion packages/iles/types/shared.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,10 @@ export interface AppContext extends PageData {
routes: RouteRecordRaw[]
}

export interface IslandContext {
app: App
}

export interface StaticPath<T = Record<string, any>> {
params: RouteParams
props: T
Expand Down Expand Up @@ -181,11 +185,15 @@ 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