Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(nuxt): server-only pages #24954

Merged
merged 51 commits into from
Feb 26, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
51 commits
Select commit Hold shift + click to select a range
0097fa4
wip test
huang-julien Nov 22, 2023
f35b414
feat: use current route path and remove component from preload
huang-julien Nov 26, 2023
338d479
Merge branch 'main' into feat/server-page
huang-julien Dec 3, 2023
283096d
fix: fix components.islands template
huang-julien Dec 5, 2023
a9caf7f
Merge remote-tracking branch 'origin/main' into feat/server-page
huang-julien Dec 5, 2023
2dc1242
test: rename SugarCounter to Counter
huang-julien Dec 5, 2023
a9d3ae8
Merge remote-tracking branch 'origin/main' into feat/server-page
huang-julien Dec 29, 2023
10ca60f
fix: stop loading indicator after server page end
huang-julien Dec 29, 2023
446b67f
Merge remote-tracking branch 'origin/main' into feat/server-page
huang-julien Feb 3, 2024
8afef3d
refactor: test by moving all to build time
huang-julien Feb 4, 2024
f939108
chore: lint
huang-julien Feb 4, 2024
70c9809
chore: LINNNTTT
huang-julien Feb 4, 2024
a29d5c2
chore: LIIIIIIIIIIIIIIIIIIIINNNNNNNNTTTTTTTTTTT
huang-julien Feb 4, 2024
ce525f5
fix: correctly laod server page in runtime
huang-julien Feb 4, 2024
b0f2f3b
[autofix.ci] apply automated fixes
autofix-ci[bot] Feb 4, 2024
0c2844b
fix: fix route file generation and introduce createIslandPage to hand…
huang-julien Feb 11, 2024
4a55e8c
Merge branch 'feat/server-page' of https://github.com/nuxt/nuxt into …
huang-julien Feb 11, 2024
a763824
[autofix.ci] apply automated fixes
autofix-ci[bot] Feb 11, 2024
9bc0e9b
fix: fix .server route detection
huang-julien Feb 11, 2024
6123b66
Merge branch 'feat/server-page' of https://github.com/nuxt/nuxt into …
huang-julien Feb 11, 2024
8703c28
[autofix.ci] apply automated fixes
autofix-ci[bot] Feb 11, 2024
9a5c498
Merge branch 'main' into feat/server-page
huang-julien Feb 14, 2024
807ff78
chore: cleanup
huang-julien Feb 14, 2024
b3b1947
docs: add meta data to docs
Feb 20, 2024
b69349d
Merge branch 'main' into feat/server-page
huang-julien Feb 20, 2024
ff927e1
chore: remove hook and remove spaces
huang-julien Feb 20, 2024
f0bdf48
fix: fix naming
huang-julien Feb 20, 2024
d0d0820
test: add basic test
huang-julien Feb 20, 2024
f7f8ef0
refactor: use mode like in the 25037 PR
huang-julien Feb 20, 2024
685078d
[autofix.ci] apply automated fixes
autofix-ci[bot] Feb 20, 2024
987e32c
fix(nuxt): fix serialization
huang-julien Feb 20, 2024
81b13b5
Merge branch 'feat/server-page' of https://github.com/nuxt/nuxt into …
huang-julien Feb 20, 2024
b259749
fix: remove chunk comment
huang-julien Feb 20, 2024
0e20478
chore: remove unwanted line/space changes
huang-julien Feb 21, 2024
5292d9a
chore: add back tab
huang-julien Feb 21, 2024
b673a33
Apply suggestions from code review
huang-julien Feb 24, 2024
cfb5708
fix: move url path to context
huang-julien Feb 25, 2024
f40e653
Merge branch 'feat/server-page' of https://github.com/nuxt/nuxt into …
huang-julien Feb 25, 2024
bff2a85
docs: add warning about query not being send to server pages
huang-julien Feb 25, 2024
d969135
chore: update import
danielroe Feb 25, 2024
67b697c
docs: update slightly
danielroe Feb 25, 2024
9a29f0c
perf: use single dynamic import
danielroe Feb 25, 2024
a01b82e
Merge remote-tracking branch 'origin/main' into feat/server-page
danielroe Feb 25, 2024
3207fd6
fix: allow setting 'all' as a mode
danielroe Feb 25, 2024
51a30d7
perf: simplify
danielroe Feb 25, 2024
afd8cb8
refactor: only enable server pages at build with `.server.vue` and in…
danielroe Feb 26, 2024
2bbef5b
Merge remote-tracking branch 'origin/main' into feat/server-page
danielroe Feb 26, 2024
948c374
fix: pass query for non-prerendered server pages
danielroe Feb 26, 2024
3a1262a
fix: use route, not normalised page
danielroe Feb 26, 2024
523d55e
perf: use more exact import
danielroe Feb 26, 2024
6a64292
fix: don't pass attrs/props
danielroe Feb 26, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
12 changes: 10 additions & 2 deletions docs/2.guide/2.directory-structure/1.pages.md
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
@@ -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
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
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
Expand Up @@ -14,6 +14,7 @@

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

enum SegmentParserState {
initial,
Expand Down Expand Up @@ -58,6 +59,7 @@

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 @@

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

Expand All @@ -87,6 +90,13 @@
// 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 @@ -183,7 +193,7 @@
try {
extractedMeta[key] = JSON.parse(runInNewContext(`JSON.stringify(${valueString})`, {}))
} catch {
console.debug(`[nuxt] Skipping extraction of \`${key}\` metadata as it is not JSON-serializable (reading \`${absolutePath}\`).`)

Check warning on line 196 in packages/nuxt/src/pages/utils.ts

View workflow job for this annotation

GitHub Actions / code

Unexpected console statement
dynamicProperties.add(key)
continue
}
Expand All @@ -196,7 +206,7 @@
continue
}
if (element.type !== 'Literal' || typeof element.value !== 'string') {
console.debug(`[nuxt] Skipping extraction of \`${key}\` metadata as it is not an array of string literals (reading \`${absolutePath}\`).`)

Check warning on line 209 in packages/nuxt/src/pages/utils.ts

View workflow job for this annotation

GitHub Actions / code

Unexpected console statement
dynamicProperties.add(key)
continue
}
Expand All @@ -207,7 +217,7 @@
}

if (property.value.type !== 'Literal' || typeof property.value.value !== 'string') {
console.debug(`[nuxt] Skipping extraction of \`${key}\` metadata as it is not a string literal or array of string literals (reading \`${absolutePath}\`).`)

Check warning on line 220 in packages/nuxt/src/pages/utils.ts

View workflow job for this annotation

GitHub Actions / code

Unexpected console statement
dynamicProperties.add(key)
continue
}
Expand Down Expand Up @@ -439,7 +449,18 @@
meta: `${metaImportName} || {}`,
alias: `${metaImportName}?.alias || []`,
redirect: `${metaImportName}?.redirect`,
component: genDynamicImport(file, { interopDefault: true })
component: page.mode === 'server'
? `() => createIslandPage(${route.name})`
Fixed Show fixed Hide fixed
Dismissed Show dismissed Hide dismissed
: 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
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
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
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
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
@@ -0,0 +1,10 @@
<template>
<div id="server-page">
Hello this is a server page
<NuxtLink
to="/"
>
to home
</NuxtLink>
</div>
</template>