Skip to content

Commit

Permalink
feat(nuxt): server-only pages (#24954)
Browse files Browse the repository at this point in the history
  • Loading branch information
huang-julien committed Feb 26, 2024
1 parent c052399 commit 196223c
Show file tree
Hide file tree
Showing 10 changed files with 109 additions and 5 deletions.
12 changes: 10 additions & 2 deletions docs/2.guide/2.directory-structure/1.pages.md
Original file line number Diff line number Diff line change
Expand Up @@ -357,13 +357,21 @@ function navigate(){
</script>
```

## Custom routing
## Server-Only Pages

You can define a page as [server only](/docs/guide/directory-structure/components#server-components) by giving it a `.server.vue` suffix. While you will be able to navigate to the page using client-side navigation, controlled by `vue-router`, it will be rendered with a server component automatically, meaning the code required to render the page will not be in your client-side bundle.

::note
You will also need to enable `experimental.componentIslands` in order to make this possible.
::

## Custom Routing

As your app gets bigger and more complex, your routing might require more flexibility. For this reason, Nuxt directly exposes the router, routes and router options for customization in different ways.

:read-more{to="/docs/guide/going-further/custom-routing"}

## Multiple pages directories
## Multiple Pages Directories

By default, all your pages should be in one `pages` directory at the root of your project.

Expand Down
32 changes: 32 additions & 0 deletions packages/nuxt/src/components/runtime/server-component.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import { defineComponent, h, ref } from 'vue'
import NuxtIsland from '#app/components/nuxt-island'
import { useRoute } from '#app/composables/router'
import { isPrerendered } from '#app/composables/payload'

/*@__NO_SIDE_EFFECTS__*/
export const createServerComponent = (name: string) => {
Expand All @@ -25,3 +27,33 @@ export const createServerComponent = (name: string) => {
}
})
}

/*@__NO_SIDE_EFFECTS__*/
export const createIslandPage = (name: string) => {
return defineComponent({
name,
inheritAttrs: false,
props: { lazy: Boolean },
async setup (props, { slots, expose }) {
const islandRef = ref<null | typeof NuxtIsland>(null)

expose({
refresh: () => islandRef.value?.refresh()
})

const route = useRoute()
const path = await isPrerendered(route.path) ? route.path : route.fullPath.replace(/#.*$/, '')

return () => {
return h('div', [
h(NuxtIsland, {
name: `page:${name}`,
lazy: props.lazy,
ref: islandRef,
context: { url: path }
}, slots)
])
}
}
})
}
7 changes: 6 additions & 1 deletion packages/nuxt/src/components/templates.ts
Original file line number Diff line number Diff line change
Expand Up @@ -72,12 +72,17 @@ export const componentsIslandsTemplate: NuxtTemplate = {
// components.islands.mjs'
getContents ({ app }) {
const components = app.components
const pages = app.pages
const islands = components.filter(component =>
component.island ||
// .server components without a corresponding .client component will need to be rendered as an island
(component.mode === 'server' && !components.some(c => c.pascalName === component.pascalName && c.mode === 'client'))
)

const pageExports = pages?.filter(p => (p.mode === 'server' && p.file && p.name)).map((p) => {
return `"page:${p.name}": defineAsyncComponent(${genDynamicImport(p.file!)}.then(c => c.default || c))`
}) || []

return [
'import { defineAsyncComponent } from \'vue\'',
'export const islandComponents = import.meta.client ? {} : {',
Expand All @@ -87,7 +92,7 @@ export const componentsIslandsTemplate: NuxtTemplate = {
const comment = createImportMagicComments(c)
return ` "${c.pascalName}": defineAsyncComponent(${genDynamicImport(c.filePath, { comment })}.then(c => ${exp}))`
}
).join(',\n'),
).concat(pageExports).join(',\n'),
'}'
].join('\n')
}
Expand Down
4 changes: 4 additions & 0 deletions packages/nuxt/src/pages/module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -400,6 +400,10 @@ export default defineNuxtModule({
const sourceFiles = nuxt.apps.default?.pages?.length ? getSources(nuxt.apps.default.pages) : []

for (const key in manifest) {
if (manifest[key].src && Object.values(nuxt.apps).some(app => app.pages?.some(page => page.mode === 'server' && page.file === join(nuxt.options.srcDir, manifest[key].src!) ))) {
delete manifest[key]
continue
}
if (manifest[key].isEntry) {
manifest[key].dynamicImports =
manifest[key].dynamicImports?.filter(i => !sourceFiles.includes(i))
Expand Down
23 changes: 22 additions & 1 deletion packages/nuxt/src/pages/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import type { NuxtPage } from 'nuxt/schema'

import { uniqueBy } from '../core/utils'
import { toArray } from '../utils'
import { distDir } from '../dirs'

enum SegmentParserState {
initial,
Expand Down Expand Up @@ -58,6 +59,7 @@ export async function resolvePagesRoutes (): Promise<NuxtPage[]> {

const allRoutes = await generateRoutesFromFiles(uniqueBy(scannedFiles, 'relativePath'), {
shouldExtractBuildMeta: nuxt.options.experimental.scanPageMeta || nuxt.options.experimental.typedPages,
shouldUseServerComponents: !!nuxt.options.experimental.componentIslands,
vfs: nuxt.vfs
})

Expand All @@ -66,6 +68,7 @@ export async function resolvePagesRoutes (): Promise<NuxtPage[]> {

type GenerateRoutesFromFilesOptions = {
shouldExtractBuildMeta?: boolean
shouldUseServerComponents?: boolean
vfs?: Record<string, string>
}

Expand All @@ -87,6 +90,13 @@ export async function generateRoutesFromFiles (files: ScannedFile[], options: Ge
// Array where routes should be added, useful when adding child routes
let parent = routes

if (segments[segments.length - 1].endsWith('.server')) {
segments[segments.length - 1] = segments[segments.length - 1].replace('.server', '')
if (options.shouldUseServerComponents) {
route.mode = 'server'
}
}

for (let i = 0; i < segments.length; i++) {
const segment = segments[i]

Expand Down Expand Up @@ -439,7 +449,18 @@ export function normalizeRoutes (routes: NuxtPage[], metaImports: Set<string> =
meta: `${metaImportName} || {}`,
alias: `${metaImportName}?.alias || []`,
redirect: `${metaImportName}?.redirect`,
component: genDynamicImport(file, { interopDefault: true })
component: page.mode === 'server'
? `() => createIslandPage(${route.name})`
: genDynamicImport(file, { interopDefault: true })
}

if (page.mode === 'server') {
metaImports.add(`
let _createIslandPage
async function createIslandPage (name) {
_createIslandPage ||= await import(${JSON.stringify(resolve(distDir, 'components/runtime/server-component'))}).then(r => r.createIslandPage)
return _createIslandPage(name)
};`)
}

if (route.children != null) {
Expand Down
3 changes: 2 additions & 1 deletion packages/nuxt/test/pages.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ describe('pages:generateRoutesFromFiles', () => {
},
}
})

const tests: Array<{
description: string
files?: Array<{ path: string; template?: string; }>
Expand Down Expand Up @@ -570,6 +570,7 @@ describe('pages:generateRoutesFromFiles', () => {

try {
result = await generateRoutesFromFiles(test.files.map(file => ({
shouldUseServerComponents: true,
absolutePath: file.path,
relativePath: file.path.replace(/^(pages|layer\/pages)\//, '')
})), { shouldExtractBuildMeta: true, vfs })
Expand Down
9 changes: 9 additions & 0 deletions packages/schema/src/types/hooks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,15 @@ export type NuxtPage = {
alias?: string[] | string
redirect?: RouteLocationRaw
children?: NuxtPage[]
/**
* Set the render mode.
*
* `all` means the page will be rendered isomorphically - with JavaScript both on client and server.
*
* `server` means pages are automatically rendered with server components, so there will be no JavaScript to render the page in your client bundle.
* @default 'all'
*/
mode?: 'server' | 'all'
}

export type NuxtMiddleware = {
Expand Down
11 changes: 11 additions & 0 deletions test/basic.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2090,6 +2090,17 @@ describe('component islands', () => {

await startServer()
})

it('render island page', async () => {
const { page } = await renderPage('/')

const islandPageRequest = page.waitForRequest((req) => {
return req.url().includes('/__nuxt_island/page:server-page')
})
await page.getByText('to server page').click()
await islandPageRequest
await page.locator('#server-page').waitFor()
})
})

describe.runIf(isDev() && !isWebpack)('vite plugins', () => {
Expand Down
3 changes: 3 additions & 0 deletions test/fixtures/basic/pages/index.vue
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,9 @@
<NuxtLink to="/big-page-1">
to big 1
</NuxtLink>
<NuxtLink to="/server-page">
to server page
</NuxtLink>
</div>
</template>

Expand Down
10 changes: 10 additions & 0 deletions test/fixtures/basic/pages/server-page.server.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
<template>
<div id="server-page">
Hello this is a server page
<NuxtLink
to="/"
>
to home
</NuxtLink>
</div>
</template>

0 comments on commit 196223c

Please sign in to comment.