diff --git a/docs/src/pages/config/index.mdx b/docs/src/pages/config/index.mdx
index a9232c33..2c7201fa 100644
--- a/docs/src/pages/config/index.mdx
+++ b/docs/src/pages/config/index.mdx
@@ -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
@@ -158,16 +159,23 @@ Whether to output more information about islands and hydration in development.
## Your App
- will pre-configure a Vue 3 app that will load any [pages] defined in the
-site.
+ 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: [
@@ -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`
+
+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`
+
+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)
+ }
+ },
})
```
@@ -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`
+- **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`
@@ -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`
diff --git a/packages/hydration/hydration.ts b/packages/hydration/hydration.ts
index 6c4bfc68..19a734d8 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()
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..3d9100ce 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)
diff --git a/packages/iles/src/client/app/components/Island.vue b/packages/iles/src/client/app/components/Island.vue
index d8978932..5ac60eea 100644
--- a/packages/iles/src/client/app/components/Island.vue
+++ b/packages/iles/src/client/app/components/Island.vue
@@ -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)
`
}
diff --git a/packages/iles/src/node/alias.ts b/packages/iles/src/node/alias.ts
index aa5fa37d..bde99bfa 100644
--- a/packages/iles/src/node/alias.ts
+++ b/packages/iles/src/node/alias.ts
@@ -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}`
@@ -40,6 +43,7 @@ export function resolveAliases (root: string, userConfig: UserConfig): AliasOpti
const paths: Record = {
'/@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,
}
@@ -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'),
diff --git a/packages/iles/src/node/config.ts b/packages/iles/src/node/config.ts
index 8a5d84f0..9bd1d0d1 100644
--- a/packages/iles/src/node/config.ts
+++ b/packages/iles/src/node/config.ts
@@ -310,6 +310,7 @@ function viteConfigDefaults (root: string, userConfig: UserConfig): ViteOptions
'@islands/hydration',
'@islands/prerender',
'vue/server-renderer',
+ 'virtual:user-app',
],
},
}
diff --git a/packages/iles/src/node/plugin/plugin.ts b/packages/iles/src/node/plugin/plugin.ts
index 5b29d434..58e367f8 100644
--- a/packages/iles/src/node/plugin/plugin.ts
+++ b/packages/iles/src/node/plugin/plugin.ts
@@ -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'
@@ -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,
diff --git a/packages/iles/types/shared.d.ts b/packages/iles/types/shared.d.ts
index 5c23cf59..30a0afce 100644
--- a/packages/iles/types/shared.d.ts
+++ b/packages/iles/types/shared.d.ts
@@ -80,6 +80,10 @@ export interface AppContext extends PageData {
routes: RouteRecordRaw[]
}
+export interface IslandContext {
+ app: App
+}
+
export interface StaticPath> {
params: RouteParams
props: T
@@ -181,11 +185,15 @@ 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